generated from nhcarrigan/template
feat: migrate from github
This commit is contained in:
28
src/modules/buttons/handleCopyIdButton.ts
Normal file
28
src/modules/buttons/handleCopyIdButton.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { ButtonInteraction } from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Handles the logic for the acknowledge button on message reports.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {ButtonInteraction} interaction The interaction payload from Discord.
|
||||
*/
|
||||
export const handleCopyIdButton = async (
|
||||
bot: ExtendedClient,
|
||||
interaction: ButtonInteraction
|
||||
) => {
|
||||
try {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const id = interaction.customId.split("-")[1];
|
||||
await interaction.editReply({
|
||||
content: id || "Unable to parse ID."
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "handle copy id button", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
43
src/modules/buttons/handleReportAcknowledgeButton.ts
Normal file
43
src/modules/buttons/handleReportAcknowledgeButton.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ButtonInteraction } from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Handles the logic for the acknowledge button on message reports.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {ButtonInteraction} interaction The interaction payload from Discord.
|
||||
*/
|
||||
export const handleReportAcknowledgeButton = async (
|
||||
bot: ExtendedClient,
|
||||
interaction: ButtonInteraction
|
||||
) => {
|
||||
try {
|
||||
await interaction.deferUpdate();
|
||||
const message = interaction.message;
|
||||
const embed = message.embeds[0];
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
{
|
||||
title: embed?.title || "wtf",
|
||||
description: embed?.description || "wtf",
|
||||
fields: [
|
||||
...(embed?.fields ?? []),
|
||||
{
|
||||
name: "Acknowledged by",
|
||||
value: `<@${interaction.user.id}>`
|
||||
}
|
||||
],
|
||||
color: 0x00ff00
|
||||
}
|
||||
],
|
||||
components: []
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "handle report acknowledge button", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
24
src/modules/commands/calculateMuteDuration.ts
Normal file
24
src/modules/commands/calculateMuteDuration.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Parses a value/unit pair into a number of milliseconds. For example,
|
||||
* (1, "seconds") would return one second in milliseconds.
|
||||
*
|
||||
* @param {number} value The number of "unit" to convert to milliseconds.
|
||||
* @param {string} unit The unit of time to convert to milliseconds.
|
||||
* @returns {number} The number of milliseconds.
|
||||
*/
|
||||
export const calculateMuteDuration = (value: number, unit: string) => {
|
||||
switch (unit) {
|
||||
case "seconds":
|
||||
return value * 1000;
|
||||
case "minutes":
|
||||
return value * 60000;
|
||||
case "hours":
|
||||
return value * 3600000;
|
||||
case "days":
|
||||
return value * 86400000;
|
||||
case "weeks":
|
||||
return value * 604800000;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
213
src/modules/commands/generateProfileImage.ts
Normal file
213
src/modules/commands/generateProfileImage.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { levels } from "@prisma/client";
|
||||
import { AttachmentBuilder } from "discord.js";
|
||||
import nodeHtmlToImage from "node-html-to-image";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Creates an image from the user's profile settings, converts it into a Discord
|
||||
* attachment, and returns it.
|
||||
*
|
||||
* @param {ExtendedClient} CamperChan The CamperChan's Discord instance.
|
||||
* @param {levels} record The user's record from the database.
|
||||
* @returns {AttachmentBuilder} The attachment, or null on error.
|
||||
*/
|
||||
export const generateProfileImage = async (
|
||||
CamperChan: ExtendedClient,
|
||||
record: levels
|
||||
): Promise<AttachmentBuilder | null> => {
|
||||
try {
|
||||
const {
|
||||
avatar,
|
||||
backgroundColour,
|
||||
backgroundImage,
|
||||
colour,
|
||||
username,
|
||||
level,
|
||||
points
|
||||
} = record;
|
||||
|
||||
const html = `
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700");
|
||||
}
|
||||
body {
|
||||
background: url(${backgroundImage}) no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
text-align: center;
|
||||
font-family: "Roboto", Courier, monospace;
|
||||
font-size: 75px;
|
||||
padding: 2.5%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 150px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #${backgroundColour}bf;
|
||||
color: #${colour};
|
||||
padding: 2.5%;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 125px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 250px auto;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<main>
|
||||
<div class="header">
|
||||
<img class="avatar" src=${avatar || "https://cdn.freecodecamp.org/platform/universal/fcc_puck_500.jpg"}></img>
|
||||
<div>
|
||||
<h1>${username}</h1>
|
||||
<p>Level ${level} (${points.toLocaleString("en-GB")}xp)</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
`;
|
||||
const alt = `${username} is at level ${level} with ${points.toLocaleString("en-GB")} experience points.`;
|
||||
|
||||
const image = await nodeHtmlToImage({
|
||||
html,
|
||||
selector: "body",
|
||||
transparent: true
|
||||
});
|
||||
|
||||
if (!(image instanceof Buffer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachment = new AttachmentBuilder(image, {
|
||||
name: `${username}.png`,
|
||||
description: alt
|
||||
});
|
||||
|
||||
return attachment;
|
||||
} catch (err) {
|
||||
await errorHandler(CamperChan, "generate profile image module", err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the image for the leaderboard.
|
||||
*
|
||||
* @param {ExtendedClient} CamperChan The CamperChan's Discord instance.
|
||||
* @param {levels} levels The user's record from the database.
|
||||
* @returns {AttachmentBuilder} The attachment, or null on error.
|
||||
*/
|
||||
export const generateLeaderboardImage = async (
|
||||
CamperChan: ExtendedClient,
|
||||
levels: (levels & { index: number })[]
|
||||
): Promise<AttachmentBuilder | null> => {
|
||||
try {
|
||||
const html = `
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700");
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
height: 4100px;
|
||||
width: 2510px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 150px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 2500px;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 2250px;
|
||||
height: 400px;
|
||||
margin: 5px 10px;
|
||||
justify-items: left;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
${levels.map(
|
||||
(l) =>
|
||||
`<div class="row" style="background-color: #${l.backgroundColour || "0a0a23"}bf;color: #${l.colour || "d0d0d5"};padding: 2.5%;border-radius: 100px;">
|
||||
<img style="border-radius: 50%;" src=${l.avatar || "https://cdn.freecodecamp.org/platform/universal/fcc_puck_500.jpg"}></img>
|
||||
<div style="text-align: left;padding-left:100px;">
|
||||
<h1>#${l.index}. ${l.username}</h1>
|
||||
<p>Level ${l.level} (${l.points}xp)</p>
|
||||
</div>
|
||||
</div>`
|
||||
)}
|
||||
</body>
|
||||
`;
|
||||
const alt = levels
|
||||
.map(
|
||||
(l) =>
|
||||
`${l.username} is rank ${l.index} at ${l.level} with ${l.points.toLocaleString("en-GB")} experience points.`
|
||||
)
|
||||
.join(", ");
|
||||
|
||||
const image = await nodeHtmlToImage({
|
||||
html,
|
||||
selector: "body",
|
||||
transparent: true
|
||||
});
|
||||
|
||||
if (!(image instanceof Buffer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachment = new AttachmentBuilder(image, {
|
||||
name: `leaderboard-${levels[0]?.index}.png`,
|
||||
description: alt
|
||||
});
|
||||
|
||||
return attachment;
|
||||
} catch (err) {
|
||||
await errorHandler(CamperChan, "generate leaderboard image module", err);
|
||||
return null;
|
||||
}
|
||||
};
|
32
src/modules/commands/profileValidation.ts
Normal file
32
src/modules/commands/profileValidation.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Checks if a string matches a 6 character hex code.
|
||||
*
|
||||
* @param {string} colour The colour code to validate.
|
||||
* @returns {boolean} If the string is in the correct format.
|
||||
*/
|
||||
export const validateColour = (colour: string): boolean => {
|
||||
return /[\da-f]{6}/gi.test(colour);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a url points to a valid image.
|
||||
*
|
||||
* @param {string} url The URL to validate.
|
||||
* @returns {boolean} If the URL provides a 2XX response, and if the response content type
|
||||
* is an image.
|
||||
*/
|
||||
export const validateImage = async (url: string): Promise<boolean> => {
|
||||
const validImage = await fetch(url, {
|
||||
method: "HEAD"
|
||||
}).catch(() => null);
|
||||
|
||||
if (!validImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validImage.headers.get("content-type")?.startsWith("image/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
39
src/modules/data/getConfig.ts
Normal file
39
src/modules/data/getConfig.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { configs } from "@prisma/client";
|
||||
|
||||
import { defaultConfig } from "../../config/DefaultConfig";
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Module to get the server config from the cache, database, or default.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {string} serverId The ID of the server to get the config for.
|
||||
* @returns {ExtendedClient["config"]} The server config.
|
||||
*/
|
||||
export const getConfig = async (
|
||||
bot: ExtendedClient,
|
||||
serverId: string
|
||||
): Promise<Omit<configs, "id">> => {
|
||||
try {
|
||||
const exists = bot.configs[serverId];
|
||||
if (exists) {
|
||||
return exists;
|
||||
}
|
||||
const record = await bot.db.configs.upsert({
|
||||
where: {
|
||||
serverId
|
||||
},
|
||||
create: {
|
||||
...defaultConfig,
|
||||
serverId
|
||||
},
|
||||
update: {}
|
||||
});
|
||||
bot.configs[serverId] = record;
|
||||
return record;
|
||||
} catch (err) {
|
||||
await errorHandler(bot, "get config", err);
|
||||
return defaultConfig;
|
||||
}
|
||||
};
|
40
src/modules/data/setConfig.ts
Normal file
40
src/modules/data/setConfig.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { defaultConfig } from "../../config/DefaultConfig";
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Module to update a config.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {string} serverId The ID of the server to update.
|
||||
* @param {keyof ExtendedClient["config"]} setting The setting to update.
|
||||
* @param {number | string} value The value to update the setting to.
|
||||
* @returns {boolean} True on successful update.
|
||||
*/
|
||||
export const setConfig = async (
|
||||
bot: ExtendedClient,
|
||||
serverId: string,
|
||||
setting: keyof ExtendedClient["configs"][""],
|
||||
value: number | string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const configData = await bot.db.configs.upsert({
|
||||
where: {
|
||||
serverId
|
||||
},
|
||||
create: {
|
||||
...defaultConfig,
|
||||
serverId,
|
||||
[setting]: value
|
||||
},
|
||||
update: {
|
||||
[setting]: value
|
||||
}
|
||||
});
|
||||
bot.configs[serverId] = configData;
|
||||
return true;
|
||||
} catch (err) {
|
||||
await errorHandler(bot, "set config", err);
|
||||
return false;
|
||||
}
|
||||
};
|
46
src/modules/events/checkSpamDomain.ts
Normal file
46
src/modules/events/checkSpamDomain.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Checks if a domain is a known source of Discord scams.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {string} domain The domain to validate. DO NOT include the protocol or path.
|
||||
* @returns {boolean} True if the domain is known as a scam.
|
||||
*/
|
||||
export const checkSpamDomain = async (
|
||||
bot: ExtendedClient,
|
||||
domain: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const walshyReq = await fetch("https://bad-domains.walshy.dev/check", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Identity": "Naomi's mod bot - built by naomi_lgbt"
|
||||
},
|
||||
body: JSON.stringify({ domain })
|
||||
});
|
||||
const walshyRes = (await walshyReq.json()) as { badDomain: boolean };
|
||||
if (walshyRes.badDomain) {
|
||||
return true;
|
||||
}
|
||||
const yachtsReq = await fetch(
|
||||
`https://phish.sinking.yachts/v2/check/${encodeURI(domain)}`,
|
||||
{
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Identity": "Naomi's mod bot - built by naomi_lgbt"
|
||||
}
|
||||
}
|
||||
);
|
||||
const yachtsRes = (await yachtsReq.json()) as boolean;
|
||||
if (yachtsRes) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
await errorHandler(bot, "load spam domains", err);
|
||||
return false;
|
||||
}
|
||||
};
|
35
src/modules/events/getModActionFromAuditLog.ts
Normal file
35
src/modules/events/getModActionFromAuditLog.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { AuditLogEvent, GuildAuditLogsEntry } from "discord.js";
|
||||
|
||||
import { Action } from "../../interfaces/Action";
|
||||
|
||||
/**
|
||||
* Module to parse the audit log entry and return the moderation action.
|
||||
*
|
||||
* @param {GuildAuditLogsEntry} log The audit log entry payload.
|
||||
* @returns {Action | null} The mod action string, or null if not found.
|
||||
*/
|
||||
export const getModActionFromAuditLog = (
|
||||
log: GuildAuditLogsEntry
|
||||
): Action | null => {
|
||||
const muteChange = log.changes.find(
|
||||
(change) => change.key === "communication_disabled_until"
|
||||
);
|
||||
switch (log.action) {
|
||||
case AuditLogEvent.MemberBanAdd:
|
||||
return "ban";
|
||||
case AuditLogEvent.MemberBanRemove:
|
||||
return "unban";
|
||||
case AuditLogEvent.MemberKick:
|
||||
return "kick";
|
||||
case AuditLogEvent.MemberUpdate:
|
||||
if (!muteChange) {
|
||||
return null;
|
||||
}
|
||||
if (muteChange.new) {
|
||||
return "mute";
|
||||
}
|
||||
return "unmute";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
55
src/modules/interactions/handleChatInputCommand.ts
Normal file
55
src/modules/interactions/handleChatInputCommand.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { ChatInputCommandInteraction } from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
import { isModerator } from "../../utils/isModerator";
|
||||
import { isGuildCommandInteraction } from "../validateGuildCommands";
|
||||
|
||||
/**
|
||||
* Handles the logic for running slash commands.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {ChatInputCommandInteraction} interaction The interaction payload from Discord.
|
||||
*/
|
||||
export const handleChatInputCommand = async (
|
||||
bot: ExtendedClient,
|
||||
interaction: ChatInputCommandInteraction
|
||||
) => {
|
||||
try {
|
||||
if (!isGuildCommandInteraction(interaction)) {
|
||||
await interaction.reply({
|
||||
content: "You can only use commands in a server.",
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
const command = bot.commands.find(
|
||||
(c) => c.data.name === interaction.commandName
|
||||
);
|
||||
if (!command) {
|
||||
await interaction.reply({
|
||||
content: "That's not a valid command. Please contact Naomi.",
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(!interaction.member || !isModerator(interaction.member)) &&
|
||||
!["leaderboard", "rank", "profile", "role", "help", "ping"].includes(
|
||||
interaction.commandName
|
||||
)
|
||||
) {
|
||||
await interaction.reply({
|
||||
content: "You must be a moderator to use bot commands.",
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
await command.run(bot, interaction);
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "handle chat input command", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
42
src/modules/interactions/handleContextMenuCommand.ts
Normal file
42
src/modules/interactions/handleContextMenuCommand.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { ContextMenuCommandInteraction } from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
import { isGuildContextInteraction } from "../validateGuildCommands";
|
||||
|
||||
/**
|
||||
* Handles the logic for running context commands.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {ContextMenuCommandInteraction} interaction The interaction payload from Discord.
|
||||
*/
|
||||
export const handleContextMenuCommand = async (
|
||||
bot: ExtendedClient,
|
||||
interaction: ContextMenuCommandInteraction
|
||||
) => {
|
||||
try {
|
||||
if (!isGuildContextInteraction(interaction)) {
|
||||
await interaction.reply({
|
||||
content: "You can only use this in a server.",
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
const context = bot.contexts.find(
|
||||
(c) => c.data.name === interaction.commandName
|
||||
);
|
||||
if (!context) {
|
||||
await interaction.reply({
|
||||
content: "That's not a valid context. Please contact Naomi.",
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
await context.run(bot, interaction);
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "handle context menu command", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
127
src/modules/modals/handleMassBanModal.ts
Normal file
127
src/modules/modals/handleMassBanModal.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
EmbedBuilder,
|
||||
Message,
|
||||
ModalSubmitInteraction
|
||||
} from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
import { getConfig } from "../data/getConfig";
|
||||
|
||||
/**
|
||||
* Handles the submission of the mass ban form.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {ModalSubmitInteraction} interaction The interaction payload from Discord.
|
||||
*/
|
||||
export const handleMassBanModal = async (
|
||||
bot: ExtendedClient,
|
||||
interaction: ModalSubmitInteraction
|
||||
) => {
|
||||
try {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const { guild } = interaction;
|
||||
if (!guild) {
|
||||
await interaction.editReply({
|
||||
content: "This command can only be used in a guild."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rawBanList = interaction.fields.getTextInputValue("mass-ban-ids");
|
||||
const banList = rawBanList.trim().split(/\b/g);
|
||||
|
||||
const reason = interaction.fields.getTextInputValue("reason");
|
||||
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTitle("Confirm Mass Ban of Following IDs:");
|
||||
embed.setDescription(banList.join("\n"));
|
||||
embed.addFields({
|
||||
name: "Reason",
|
||||
value: reason
|
||||
});
|
||||
|
||||
const yes = new ButtonBuilder()
|
||||
.setCustomId("confirm")
|
||||
.setLabel("Confirm")
|
||||
.setStyle(ButtonStyle.Success);
|
||||
const no = new ButtonBuilder()
|
||||
.setCustomId("cancel")
|
||||
.setLabel("Cancel")
|
||||
.setStyle(ButtonStyle.Danger);
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(yes, no);
|
||||
const response = (await interaction.editReply({
|
||||
embeds: [embed],
|
||||
components: [row]
|
||||
})) as Message;
|
||||
|
||||
const collector =
|
||||
response.createMessageComponentCollector<ComponentType.Button>({
|
||||
filter: (click) => click.user.id === interaction.user.id,
|
||||
time: 10000,
|
||||
max: 1
|
||||
});
|
||||
|
||||
collector.on("end", async (clicks) => {
|
||||
const choice = clicks.first()?.customId;
|
||||
if (!clicks || clicks.size <= 0 || !choice) {
|
||||
await interaction.editReply({
|
||||
content: "This command has timed out.",
|
||||
embeds: [],
|
||||
components: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice === "confirm") {
|
||||
for (const id of banList) {
|
||||
await guild.bans.create(id, {
|
||||
reason: `Massban by ${interaction.user.tag} for: ${reason}`,
|
||||
deleteMessageSeconds: 86400
|
||||
});
|
||||
}
|
||||
|
||||
const config = await getConfig(bot, guild.id);
|
||||
if (!config.modLogChannel) {
|
||||
return;
|
||||
}
|
||||
const channel =
|
||||
guild.channels.cache.get(config.modLogChannel) ||
|
||||
(await guild.channels.fetch(config.modLogChannel));
|
||||
|
||||
if (!channel || !("send" in channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
embed.setTitle("Mass Ban:");
|
||||
embed.setAuthor({
|
||||
name: interaction.user.tag,
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
});
|
||||
await channel.send({ embeds: [embed] });
|
||||
await interaction.editReply({
|
||||
content: "Mass ban complete.",
|
||||
embeds: [],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
|
||||
if (choice === "cancel") {
|
||||
interaction.editReply({
|
||||
content: "Mass ban cancelled.",
|
||||
embeds: [],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "handle mass ban modal", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
82
src/modules/modals/handleMessageReportModal.ts
Normal file
82
src/modules/modals/handleMessageReportModal.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { ModalSubmitInteraction } from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../utils/errorHandler";
|
||||
import { getConfig } from "../data/getConfig";
|
||||
|
||||
/**
|
||||
* Handles the submission of the message report form.
|
||||
*
|
||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
||||
* @param {ModalSubmitInteraction} interaction The interaction payload from Discord.
|
||||
*/
|
||||
export const handleMessageReportModal = async (
|
||||
bot: ExtendedClient,
|
||||
interaction: ModalSubmitInteraction
|
||||
) => {
|
||||
try {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
if (!interaction.guild) {
|
||||
await interaction.editReply({
|
||||
content: "This command can only be used in a guild."
|
||||
});
|
||||
return;
|
||||
}
|
||||
const reportLogId = interaction.customId.split("-")[1] ?? "oops";
|
||||
const config = await getConfig(bot, interaction.guild.id);
|
||||
|
||||
if (!config.messageReportChannel) {
|
||||
await interaction.editReply({
|
||||
content: "Reporting has not been set up for this server."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const channel =
|
||||
interaction.guild.channels.cache.get(config.messageReportChannel) ||
|
||||
(await interaction.guild.channels.fetch(config.messageReportChannel));
|
||||
|
||||
if (!channel || !("send" in channel)) {
|
||||
await interaction.editReply({
|
||||
content: "Reporting channel not found."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reportLog = await channel.messages
|
||||
.fetch(reportLogId)
|
||||
.catch(() => null);
|
||||
if (!reportLog) {
|
||||
await interaction.editReply({
|
||||
content: "Could not find the report log."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = reportLog.embeds[0];
|
||||
await reportLog.edit({
|
||||
embeds: [
|
||||
{
|
||||
title: embed?.title || "wtf",
|
||||
description: embed?.description || "wtf",
|
||||
fields: [
|
||||
...(embed?.fields ?? []),
|
||||
{
|
||||
name: "Reason",
|
||||
value: interaction.fields.getTextInputValue("reason")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: "Your report has been submitted."
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "handle message report modal", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
34
src/modules/subcommands/config/handleAppealLink.ts
Normal file
34
src/modules/subcommands/config/handleAppealLink.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { CommandHandler } from "../../../interfaces/CommandHandler";
|
||||
import { errorHandler } from "../../../utils/errorHandler";
|
||||
import { setConfig } from "../../data/setConfig";
|
||||
|
||||
/**
|
||||
* Sets the ban appeal link for the server.
|
||||
*/
|
||||
export const handleAppealLink: CommandHandler = async (bot, interaction) => {
|
||||
try {
|
||||
const link = interaction.options.getString("link", true);
|
||||
|
||||
const success = await setConfig(
|
||||
bot,
|
||||
interaction.guild.id,
|
||||
"banAppealLink",
|
||||
link
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await interaction.editReply({
|
||||
content: `Members who are banned can appeal at <${link}>.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.editReply({
|
||||
content: "Failed to set the settings."
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "automod logging subcommand", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
34
src/modules/subcommands/config/handleInviteLink.ts
Normal file
34
src/modules/subcommands/config/handleInviteLink.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { CommandHandler } from "../../../interfaces/CommandHandler";
|
||||
import { errorHandler } from "../../../utils/errorHandler";
|
||||
import { setConfig } from "../../data/setConfig";
|
||||
|
||||
/**
|
||||
* Sets the invite link for the server.
|
||||
*/
|
||||
export const handleInviteLink: CommandHandler = async (bot, interaction) => {
|
||||
try {
|
||||
const link = interaction.options.getString("link", true);
|
||||
|
||||
const success = await setConfig(
|
||||
bot,
|
||||
interaction.guild.id,
|
||||
"inviteLink",
|
||||
link
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await interaction.editReply({
|
||||
content: `Members who are kicked will be invited back with <${link}>.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.editReply({
|
||||
content: "Failed to set the settings."
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "automod logging subcommand", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
141
src/modules/subcommands/config/handleList.ts
Normal file
141
src/modules/subcommands/config/handleList.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
EmbedBuilder
|
||||
} from "discord.js";
|
||||
|
||||
import { CommandHandler } from "../../../interfaces/CommandHandler";
|
||||
import { errorHandler } from "../../../utils/errorHandler";
|
||||
import { getNextIndex, getPreviousIndex } from "../../../utils/getArrayIndex";
|
||||
|
||||
/**
|
||||
* Fetches the automod settings for the given guild.
|
||||
*/
|
||||
export const handleList: CommandHandler = async (bot, interaction, config) => {
|
||||
try {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
embed.setTitle("Automod Settings");
|
||||
embed.addFields([
|
||||
{
|
||||
name: "Moderation Log Channel",
|
||||
value: config.modLogChannel ? `<#${config.modLogChannel}>` : "Not set.",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Event Log Channel",
|
||||
value: config.eventLogChannel
|
||||
? `<#${config.eventLogChannel}>`
|
||||
: "Not set.",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Message Report Channel",
|
||||
value: config.messageReportChannel
|
||||
? `<#${config.messageReportChannel}>`
|
||||
: "Not set.",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Invite Link",
|
||||
value: config.inviteLink || "None",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Ban Appeal Link",
|
||||
value: config.banAppealLink || "None",
|
||||
inline: true
|
||||
}
|
||||
]);
|
||||
|
||||
const roles = await bot.db.levelRoles.findMany({
|
||||
where: { serverId: interaction.guild.id }
|
||||
});
|
||||
|
||||
const levelRoles = new EmbedBuilder();
|
||||
levelRoles.setTitle("Level Roles");
|
||||
levelRoles.setDescription(
|
||||
roles
|
||||
.map((r) => `- <@&${r.roleId}> is assigned at level ${r.level}`)
|
||||
.join("\n") || "No roles are currently set."
|
||||
);
|
||||
|
||||
const assignRoles = await bot.db.roles.findMany({
|
||||
where: {
|
||||
serverId: interaction.guild.id
|
||||
}
|
||||
});
|
||||
const assignRolesEmbed = new EmbedBuilder();
|
||||
assignRolesEmbed.setTitle("Self-Assignable Roles");
|
||||
assignRolesEmbed.setDescription(
|
||||
assignRoles.map((r) => `<@&${r.roleId}>`).join(" ") ||
|
||||
"No roles are currently set."
|
||||
);
|
||||
const embeds = [embed, levelRoles, assignRolesEmbed];
|
||||
|
||||
let index = 0;
|
||||
const nextButton = new ButtonBuilder()
|
||||
.setCustomId("next")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setLabel(
|
||||
embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed."
|
||||
)
|
||||
.setEmoji("▶️");
|
||||
const prevButton = new ButtonBuilder()
|
||||
.setCustomId("prev")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setLabel(
|
||||
embeds[getPreviousIndex(embeds, index)]?.data.title || "Unknown embed."
|
||||
)
|
||||
.setEmoji("◀️");
|
||||
const initialRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
prevButton,
|
||||
nextButton
|
||||
);
|
||||
|
||||
const response = await interaction.editReply({
|
||||
embeds: [embeds[index] as EmbedBuilder],
|
||||
components: [initialRow]
|
||||
});
|
||||
|
||||
const collector =
|
||||
response.createMessageComponentCollector<ComponentType.Button>({
|
||||
time: 1000 * 60 * 5
|
||||
});
|
||||
|
||||
collector.on("collect", async (i) => {
|
||||
await i.deferUpdate();
|
||||
index =
|
||||
i.customId === "next"
|
||||
? getNextIndex(embeds, index)
|
||||
: getPreviousIndex(embeds, index);
|
||||
prevButton.setLabel(
|
||||
embeds[getPreviousIndex(embeds, index)]?.data.title || "Unknown embed."
|
||||
);
|
||||
nextButton.setLabel(
|
||||
embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed."
|
||||
);
|
||||
const newRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
prevButton,
|
||||
nextButton
|
||||
);
|
||||
await i.editReply({
|
||||
embeds: [embeds[index] as EmbedBuilder],
|
||||
components: [newRow]
|
||||
});
|
||||
});
|
||||
|
||||
collector.on("end", async () => {
|
||||
await interaction.editReply({
|
||||
components: []
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "automod list subcommand", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
40
src/modules/subcommands/config/handleLogging.ts
Normal file
40
src/modules/subcommands/config/handleLogging.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { logChannelChoicesMap } from "../../../config/LogChannelChoices";
|
||||
import { CommandHandler } from "../../../interfaces/CommandHandler";
|
||||
import { ExtendedClient } from "../../../interfaces/ExtendedClient";
|
||||
import { errorHandler } from "../../../utils/errorHandler";
|
||||
import { setConfig } from "../../data/setConfig";
|
||||
|
||||
/**
|
||||
* Sets the logging channel for the server.
|
||||
*/
|
||||
export const handleLogging: CommandHandler = async (bot, interaction) => {
|
||||
try {
|
||||
const logType = interaction.options.getString(
|
||||
"log-type",
|
||||
true
|
||||
) as keyof ExtendedClient["configs"][""];
|
||||
const channel = interaction.options.getChannel("channel", true);
|
||||
|
||||
const success = await setConfig(
|
||||
bot,
|
||||
interaction.guild.id,
|
||||
logType,
|
||||
channel.id
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await interaction.editReply({
|
||||
content: `Your server will log ${logChannelChoicesMap[logType]} in <#${channel.id}>.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.editReply({
|
||||
content: "Failed to set the settings."
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "automod logging subcommand", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
47
src/modules/subcommands/config/handleRole.ts
Normal file
47
src/modules/subcommands/config/handleRole.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { CommandHandler } from "../../../interfaces/CommandHandler";
|
||||
import { errorHandler } from "../../../utils/errorHandler";
|
||||
|
||||
/**
|
||||
* Toggles a role to be self-assignable or not.
|
||||
*/
|
||||
export const handleRole: CommandHandler = async (bot, interaction) => {
|
||||
try {
|
||||
const role = interaction.options.getRole("role", true);
|
||||
const exists = await bot.db.roles.findUnique({
|
||||
where: {
|
||||
serverId_roleId: {
|
||||
serverId: interaction.guild.id,
|
||||
roleId: role.id
|
||||
}
|
||||
}
|
||||
});
|
||||
if (exists) {
|
||||
await bot.db.roles.delete({
|
||||
where: {
|
||||
serverId_roleId: {
|
||||
serverId: interaction.guild.id,
|
||||
roleId: role.id
|
||||
}
|
||||
}
|
||||
});
|
||||
await interaction.editReply({
|
||||
content: `Your <@&${role.id}> role is no longer self-assignable.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
await bot.db.roles.create({
|
||||
data: {
|
||||
serverId: interaction.guild.id,
|
||||
roleId: role.id
|
||||
}
|
||||
});
|
||||
await interaction.editReply({
|
||||
content: `Your <@&${role.id}> role is now self-assignable.`
|
||||
});
|
||||
} catch (err) {
|
||||
const id = await errorHandler(bot, "automod logging subcommand", err);
|
||||
await interaction.editReply({
|
||||
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
|
||||
});
|
||||
}
|
||||
};
|
27
src/modules/validateEnv.ts
Normal file
27
src/modules/validateEnv.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { WebhookClient } from "discord.js";
|
||||
|
||||
import { ExtendedClient } from "../interfaces/ExtendedClient";
|
||||
import { logHandler } from "../utils/logHandler";
|
||||
|
||||
/**
|
||||
* Validates the environment variables and constructs the object.
|
||||
*
|
||||
* @returns {ExtendedClient["env"]} The environment variable object to attach to the bot.
|
||||
*/
|
||||
export const validateEnv = (): ExtendedClient["env"] => {
|
||||
if (
|
||||
!process.env.BOT_TOKEN ||
|
||||
!process.env.DEBUG_HOOK ||
|
||||
!process.env.MONGO_URI
|
||||
) {
|
||||
logHandler.log("error", "MIssing environment variables!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
token: process.env.BOT_TOKEN,
|
||||
debugHook: new WebhookClient({ url: process.env.DEBUG_HOOK }),
|
||||
mongoUri: process.env.MONGO_URI,
|
||||
devMode: process.env.NODE_ENV !== "production"
|
||||
};
|
||||
};
|
33
src/modules/validateGuildCommands.ts
Normal file
33
src/modules/validateGuildCommands.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
ContextMenuCommandInteraction,
|
||||
Guild,
|
||||
GuildMember
|
||||
} from "discord.js";
|
||||
|
||||
import {
|
||||
GuildCommandInteraction,
|
||||
GuildContextInteraction
|
||||
} from "../interfaces/Interactions";
|
||||
|
||||
/**
|
||||
* Validates that a slash command payload has the guild and member.
|
||||
*
|
||||
* @param {ChatInputCommandInteraction} interaction The interaction payload from Discord.
|
||||
* @returns {boolean} Whether the expected properties are present.
|
||||
*/
|
||||
export const isGuildCommandInteraction = (
|
||||
interaction: ChatInputCommandInteraction
|
||||
): interaction is GuildCommandInteraction =>
|
||||
interaction.guild instanceof Guild &&
|
||||
interaction.member instanceof GuildMember;
|
||||
|
||||
/**
|
||||
* Validates that a slash command payload has the guild and member.
|
||||
*
|
||||
* @param {ContextMenuCommandInteraction} interaction The interaction payload from Discord.
|
||||
* @returns {boolean} Whether the expected properties are present.
|
||||
*/
|
||||
export const isGuildContextInteraction = (
|
||||
interaction: ContextMenuCommandInteraction
|
||||
): interaction is GuildContextInteraction => interaction.guild instanceof Guild;
|
Reference in New Issue
Block a user