feat: migrate from github

This commit is contained in:
2024-05-12 01:52:39 -07:00
commit 7437deab71
118 changed files with 10375 additions and 0 deletions

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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;
}
};

View 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;
}
};

View 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;
};

View 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;
}
};

View 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;
}
};

View 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;
}
};

View 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;
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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}\``
});
}
};

View 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"
};
};

View 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;