Compare commits

..

5 commits

Author SHA1 Message Date
powermaker450 afa0a70f8a If a confession is removed by deletion, delete it internally too 2024-10-13 13:44:34 -04:00
powermaker450 aeb3c70f91 Add attachments support 2024-10-13 13:36:27 -04:00
powermaker450 11404358eb Add detailed ban searching 2024-10-13 12:57:45 -04:00
powermaker450 1612fa8ca1 Add a button to submit a confession under the most recent one 2024-10-13 12:31:41 -04:00
powermaker450 a78e27e9fa Add and use some prettier preferences 2024-10-13 11:06:08 -04:00
18 changed files with 463 additions and 73 deletions

13
.prettierignore Normal file
View file

@ -0,0 +1,13 @@
# Prod
**/dist
**/node_modules
**/persist
# Files
**/.env
**/.prettierrc
**/LICENSE
**/package-lock.json
**/package.json
**/README.md
**/tsconfig.json

4
.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"trailingComma": "none",
"arrowParens": "avoid"
}

View file

@ -6,6 +6,7 @@
"scripts": { "scripts": {
"build": "rm -rf ./dist && tsc -p .", "build": "rm -rf ./dist && tsc -p .",
"start": "node ./dist/main.js", "start": "node ./dist/main.js",
"prettier": "if prettier -v >/dev/null 2>&1; then prettier . --write; else npx prettier . --write; fi",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "powermaker450", "author": "powermaker450",

View file

@ -19,5 +19,5 @@
import { Client } from "discord.js"; import { Client } from "discord.js";
export const BotClient = new Client({ export const BotClient = new Client({
intents: ["Guilds", "GuildMessages", "DirectMessages"], intents: ["Guilds", "GuildMessages", "DirectMessages"]
}); });

View file

@ -24,8 +24,8 @@ import Logger from "../utils/Logger";
const logger = new Logger("Deployer"); const logger = new Logger("Deployer");
const commandsData = Object.values(commands).map((command) => const commandsData = Object.values(commands).map(command =>
command.data.toJSON(), command.data.toJSON()
); );
const rest = new REST({ version: "9" }).setToken(BOT_TOKEN); const rest = new REST({ version: "9" }).setToken(BOT_TOKEN);
@ -35,7 +35,7 @@ export async function deployCommands({ guildId }: DeployCommandsProps) {
logger.log("Started refreshing (/) commands."); logger.log("Started refreshing (/) commands.");
await rest.put(Routes.applicationGuildCommands(BOT_ID, guildId), { await rest.put(Routes.applicationGuildCommands(BOT_ID, guildId), {
body: commandsData, body: commandsData
}); });
logger.log("Successfully reloaded (/) commands."); logger.log("Successfully reloaded (/) commands.");

View file

@ -17,35 +17,47 @@
*/ */
import { import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
CommandInteraction, CommandInteraction,
ComponentType,
EmbedBuilder, EmbedBuilder,
SlashCommandBuilder, SlashCommandBuilder,
TextChannel, TextChannel
} from "discord.js"; } from "discord.js";
import { BotClient } from "../bot"; import { BotClient } from "../bot";
import { dt } from "../main"; import { dt } from "../main";
import { StoreMan } from "../storeman"; import { StoreMan } from "../storeman";
import getRandomColor from "../utils/getRandomColor"; import getRandomColor from "../utils/getRandomColor";
import Logger from "../utils/Logger"; import Logger from "../utils/Logger";
import { submit } from "../modals";
const logger = new Logger("(/) confess"); const logger = new Logger("(/) confess");
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("confess") .setName("confess")
.setDescription("Send a confession") .setDescription("Send a confession")
.addStringOption((option) => .addStringOption(option =>
option option
.setName("message") .setName("message")
.setRequired(true) .setRequired(true)
.setDescription("What you want to confess"), .setDescription("What you want to confess")
)
.addStringOption(option =>
option
.setName("attachment")
.setDescription("The link to an image to attach (optional)")
); );
export async function execute(interaction: CommandInteraction) { export async function execute(interaction: CommandInteraction) {
// TODO: This all works as intended, but I'd like for it so be a reusable function
// instead because all of this is reused in src/main.ts:56
try { try {
if (dt.isBanned(interaction.guild?.id!, interaction.user.id)) { if (dt.isBanned(interaction.guild?.id!, interaction.user.id)) {
return interaction.reply({ return interaction.reply({
content: "You are banned from confessions in this server!", content: "You are banned from confessions in this server!",
ephemeral: true, ephemeral: true
}); });
} }
@ -53,7 +65,7 @@ export async function execute(interaction: CommandInteraction) {
return interaction.reply({ return interaction.reply({
content: content:
"The bot hasn't been set up yet! Ask the server admins to set it up.", "The bot hasn't been set up yet! Ask the server admins to set it up.",
ephemeral: true, ephemeral: true
}); });
} }
@ -62,7 +74,12 @@ export async function execute(interaction: CommandInteraction) {
const adminChannel = dt.getGuildInfo(interaction.guild?.id!)?.settings const adminChannel = dt.getGuildInfo(interaction.guild?.id!)?.settings
.modChannel; .modChannel;
// @ts-ignore // @ts-ignore
const messageContent = interaction.options.getString("message"); const messageContent: string = interaction.options.getString("message");
// @ts-ignore
const attachment: string = interaction.options.getString("attachment");
const isAttachment = (text: string) =>
text && (text.startsWith("http://") || text.startsWith("https://"));
const color = getRandomColor(); const color = getRandomColor();
const messageId = StoreMan.genId(); const messageId = StoreMan.genId();
@ -72,6 +89,8 @@ export async function execute(interaction: CommandInteraction) {
// @ts-ignore // @ts-ignore
.setDescription(messageContent); .setDescription(messageContent);
isAttachment(attachment) && userConfessionEmbed.setImage(attachment);
const adminConfessionEmbed = new EmbedBuilder() const adminConfessionEmbed = new EmbedBuilder()
.setColor(color) .setColor(color)
.setTitle(`Anonymous Confession \`${messageId}\``) .setTitle(`Anonymous Confession \`${messageId}\``)
@ -80,22 +99,44 @@ export async function execute(interaction: CommandInteraction) {
.addFields( .addFields(
{ {
name: "Author", name: "Author",
value: interaction.user.displayName, value: interaction.user.displayName
}, },
{ {
name: "Author ID", name: "Author ID",
value: interaction.user.id, value: interaction.user.id
}, }
); );
isAttachment(attachment) && adminConfessionEmbed.setImage(attachment);
const submitConfessionButton = new ButtonBuilder()
.setCustomId("submitConfession")
.setLabel("Submit a Confession")
.setStyle(ButtonStyle.Primary);
const actionRow = new ActionRowBuilder<ButtonBuilder>().setComponents(
submitConfessionButton
);
const message = await ( const message = await (
BotClient.channels.cache.get(confessChannel!) as TextChannel BotClient.channels.cache.get(confessChannel!) as TextChannel
).send({ ).send({
embeds: [userConfessionEmbed], embeds: [userConfessionEmbed],
components: [actionRow]
});
const collector = message.createMessageComponentCollector({
componentType: ComponentType.Button
});
collector.on("collect", i => {
if (i.customId === "submitConfession") {
i.showModal(submit);
}
}); });
await (BotClient.channels.cache.get(adminChannel!) as TextChannel).send({ await (BotClient.channels.cache.get(adminChannel!) as TextChannel).send({
embeds: [adminConfessionEmbed], embeds: [adminConfessionEmbed]
}); });
dt.addConfession( dt.addConfession(
@ -104,11 +145,29 @@ export async function execute(interaction: CommandInteraction) {
interaction.user.displayName, interaction.user.displayName,
interaction.user.id, interaction.user.id,
messageContent, messageContent,
attachment
); );
const confessionsLength = dt.getGuildInfo(interaction.guild?.id!)
?.confessions.length!;
if (confessionsLength >= 2) {
await (
BotClient.channels.cache.get(confessChannel!) as TextChannel
).messages
.fetch(
dt.getGuildInfo(interaction.guild?.id!)?.confessions[
confessionsLength - 2
].messageId!
)
.then(message => {
message.edit({ components: [] });
});
}
return interaction.reply({ return interaction.reply({
content: "Confession sent!", content: "Confession sent!",
ephemeral: true, ephemeral: true
}); });
} catch (err) { } catch (err) {
logger.error("An error occured:", err); logger.error("An error occured:", err);

View file

@ -19,7 +19,7 @@
import { import {
CommandInteraction, CommandInteraction,
PermissionFlagsBits, PermissionFlagsBits,
SlashCommandBuilder, SlashCommandBuilder
} from "discord.js"; } from "discord.js";
import { dt } from "../main"; import { dt } from "../main";
import Logger from "../utils/Logger"; import Logger from "../utils/Logger";
@ -29,11 +29,11 @@ const logger = new Logger("(/) confessban");
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("confessban") .setName("confessban")
.setDescription("Ban a user from submitting confessions.") .setDescription("Ban a user from submitting confessions.")
.addStringOption((option) => .addStringOption(option =>
option option
.setName("id") .setName("id")
.setDescription("The confession ID to ban") .setDescription("The confession ID to ban")
.setRequired(true), .setRequired(true)
) )
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers); .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers);
@ -41,18 +41,18 @@ export async function execute(interaction: CommandInteraction) {
const result = dt.addBan( const result = dt.addBan(
interaction.guild?.id!, interaction.guild?.id!,
// @ts-ignore // @ts-ignore
interaction.options.getString("id"), interaction.options.getString("id")
); );
try { try {
return result return result
? interaction.reply({ ? interaction.reply({
content: "User was banned.", content: "User was banned.",
ephemeral: true, ephemeral: true
}) })
: interaction.reply({ : interaction.reply({
content: "No confession with that ID was found.", content: "No confession with that ID was found.",
ephemeral: true, ephemeral: true
}); });
} catch (err) { } catch (err) {
logger.error("An error occured:", err); logger.error("An error occured:", err);

View file

@ -0,0 +1,49 @@
/*
* Confoss: Anonymous confessions for Discord, free as in freedom and price!
* Copyright (C) 2024 powermaker450
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
CommandInteraction,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { dt } from "../main";
import { BotClient } from "../bot";
export const data = new SlashCommandBuilder()
.setName("confessbanlist")
.setDescription("Get the current ban list")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages);
export async function execute(interaction: CommandInteraction) {
const bannedMembers = dt.getBans(interaction.guild?.id!);
let content = bannedMembers.length
? "Banned Members:\n"
: "There are no banned members.";
for (const member of bannedMembers) {
const identifiedMember = await BotClient.users.fetch(member.user);
content += `\n${identifiedMember.displayName} | \`${member.confessionId}\``;
}
return interaction.reply({
content: content,
ephemeral: true
});
}

View file

@ -20,7 +20,7 @@ import {
CommandInteraction, CommandInteraction,
EmbedBuilder, EmbedBuilder,
SlashCommandBuilder, SlashCommandBuilder,
TextChannel, TextChannel
} from "discord.js"; } from "discord.js";
import { dt } from "../main"; import { dt } from "../main";
import { BotClient } from "../bot"; import { BotClient } from "../bot";
@ -32,8 +32,8 @@ const logger = new Logger("(/) confessdel");
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("confessdel") .setName("confessdel")
.setDescription("Deletes a confession") .setDescription("Deletes a confession")
.addStringOption((option) => .addStringOption(option =>
option.setName("id").setDescription("The confession id").setRequired(true), option.setName("id").setDescription("The confession id").setRequired(true)
); );
export async function execute(interaction: CommandInteraction) { export async function execute(interaction: CommandInteraction) {
@ -41,7 +41,7 @@ export async function execute(interaction: CommandInteraction) {
return interaction.reply({ return interaction.reply({
content: content:
"The bot hasn't been set up yet! Ask the server admins to set it up.", "The bot hasn't been set up yet! Ask the server admins to set it up.",
ephemeral: true, ephemeral: true
}); });
} }
@ -53,7 +53,7 @@ export async function execute(interaction: CommandInteraction) {
try { try {
const confession = dt.getConfession( const confession = dt.getConfession(
interaction.guild?.id!, interaction.guild?.id!,
idVal, idVal
)?.messageId; )?.messageId;
const channelId = dt.getGuildInfo(interaction.guild?.id!)?.settings const channelId = dt.getGuildInfo(interaction.guild?.id!)?.settings
.confessChannel!; .confessChannel!;
@ -65,9 +65,9 @@ export async function execute(interaction: CommandInteraction) {
await (BotClient.channels.cache.get(channelId) as TextChannel).messages await (BotClient.channels.cache.get(channelId) as TextChannel).messages
.fetch(confession!) .fetch(confession!)
.then((e) => { .then(e => {
e.edit({ e.edit({
embeds: [emptyEmbed], embeds: [emptyEmbed]
}); });
}); });
@ -75,7 +75,7 @@ export async function execute(interaction: CommandInteraction) {
return interaction.reply({ return interaction.reply({
content: "Confession removed.", content: "Confession removed.",
ephemeral: true, ephemeral: true
}); });
} catch (err) { } catch (err) {
logger.error("An error occured:", err); logger.error("An error occured:", err);
@ -84,7 +84,7 @@ export async function execute(interaction: CommandInteraction) {
return interaction.reply({ return interaction.reply({
content: content:
"Either the confession wasn't found or you may not be allowed to remove it.", "Either the confession wasn't found or you may not be allowed to remove it.",
ephemeral: true, ephemeral: true
}); });
} }
} }

View file

@ -19,7 +19,7 @@
import { import {
CommandInteraction, CommandInteraction,
PermissionFlagsBits, PermissionFlagsBits,
SlashCommandBuilder, SlashCommandBuilder
} from "discord.js"; } from "discord.js";
import { dt } from "../main"; import { dt } from "../main";
import Logger from "../utils/Logger"; import Logger from "../utils/Logger";
@ -29,11 +29,11 @@ const logger = new Logger("(/) confesspardon");
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("confesspardon") .setName("confesspardon")
.setDescription("Unbans a user from confessions") .setDescription("Unbans a user from confessions")
.addStringOption((option) => .addStringOption(option =>
option option
.setName("id") .setName("id")
.setDescription("The confession ID to unban") .setDescription("The confession ID to unban")
.setRequired(true), .setRequired(true)
) )
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages); .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages);
@ -41,18 +41,18 @@ export function execute(interaction: CommandInteraction) {
const result = dt.removeBan( const result = dt.removeBan(
interaction.guild?.id!, interaction.guild?.id!,
// @ts-ignore // @ts-ignore
interaction.options.getString("id"), interaction.options.getString("id")
); );
try { try {
return result return result
? interaction.reply({ ? interaction.reply({
content: "User was unbanned.", content: "User was unbanned.",
ephemeral: true, ephemeral: true
}) })
: interaction.reply({ : interaction.reply({
content: "No confession with that ID was found.", content: "No confession with that ID was found.",
ephemeral: true, ephemeral: true
}); });
} catch (err) { } catch (err) {
logger.error("An error occured:", err); logger.error("An error occured:", err);

View file

@ -19,6 +19,7 @@
import * as confess from "./confess"; import * as confess from "./confess";
import * as confessdel from "./confessdel"; import * as confessdel from "./confessdel";
import * as confessban from "./confessban"; import * as confessban from "./confessban";
import * as confessbanlist from "./confessbanlist";
import * as confesspardon from "./confesspardon"; import * as confesspardon from "./confesspardon";
import * as ping from "./ping"; import * as ping from "./ping";
import * as setup from "./setup"; import * as setup from "./setup";
@ -27,7 +28,8 @@ export const commands = {
confess, confess,
confessdel, confessdel,
confessban, confessban,
confessbanlist,
confesspardon, confesspardon,
ping, ping,
setup, setup
}; };

View file

@ -23,7 +23,7 @@ import {
CommandInteraction, CommandInteraction,
ComponentType, ComponentType,
PermissionFlagsBits, PermissionFlagsBits,
SlashCommandBuilder, SlashCommandBuilder
} from "discord.js"; } from "discord.js";
import { dt } from "../main"; import { dt } from "../main";
import Logger from "../utils/Logger"; import Logger from "../utils/Logger";
@ -39,7 +39,7 @@ export async function execute(interaction: CommandInteraction) {
if (dt.checkSetup(interaction.guild?.id!)) { if (dt.checkSetup(interaction.guild?.id!)) {
return interaction.reply({ return interaction.reply({
content: "This guild has already been set up!", content: "This guild has already been set up!",
ephemeral: true, ephemeral: true
}); });
} }
@ -58,20 +58,20 @@ export async function execute(interaction: CommandInteraction) {
const response = await interaction.reply({ const response = await interaction.reply({
content: `# Let's get started, ${interaction.user.displayName}!\nFirst, let's choose a channel for your confessions.`, content: `# Let's get started, ${interaction.user.displayName}!\nFirst, let's choose a channel for your confessions.`,
ephemeral: true, ephemeral: true,
components: [channelRow], components: [channelRow]
}); });
const collector = response.createMessageComponentCollector({ const collector = response.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect, componentType: ComponentType.ChannelSelect,
time: 45_000, time: 45_000
}); });
collector.on("collect", async (i) => { collector.on("collect", async i => {
confessChannel = i.values[0]; confessChannel = i.values[0];
await i.update({ await i.update({
content: "Awesome!", content: "Awesome!",
components: [], components: []
}); });
collector.stop(); collector.stop();
@ -83,55 +83,55 @@ export async function execute(interaction: CommandInteraction) {
const logChannelRow = const logChannelRow =
new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents( new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
logChannelList, logChannelList
); );
const logResponse = await interaction.followUp({ const logResponse = await interaction.followUp({
content: "# Now, select a logging channel, for moderation purposes.", content: "# Now, select a logging channel, for moderation purposes.",
ephemeral: true, ephemeral: true,
components: [logChannelRow], components: [logChannelRow]
}); });
const logCollector = logResponse.createMessageComponentCollector({ const logCollector = logResponse.createMessageComponentCollector({
componentType: ComponentType.ChannelSelect, componentType: ComponentType.ChannelSelect,
time: 45_000, time: 45_000
}); });
logCollector.on("collect", async (ij) => { logCollector.on("collect", async ij => {
logChannel = ij.values[0]; logChannel = ij.values[0];
await ij.update({ await ij.update({
content: "Setup Complete!", content: "Setup Complete!",
components: [], components: []
}); });
dt.setup(guildId!, { dt.setup(guildId!, {
confessChannel: confessChannel, confessChannel: confessChannel,
modChannel: logChannel, modChannel: logChannel,
bans: [], bans: []
}); });
logCollector.stop(); logCollector.stop();
}); });
logCollector.on("end", (content) => { logCollector.on("end", content => {
// If there is no content, follow up with an error message. // If there is no content, follow up with an error message.
!content.size && !content.size &&
interaction.followUp({ interaction.followUp({
content: "No channel selected. Please try again.", content: "No channel selected. Please try again.",
ephemeral: true, ephemeral: true,
components: [], components: []
}); });
}); });
}); });
collector.on("end", (collected) => { collector.on("end", collected => {
// Same as above logCollector end // Same as above logCollector end
!collected.size && !collected.size &&
interaction.followUp({ interaction.followUp({
content: "No channel selected. Try again.", content: "No channel selected. Try again.",
ephemeral: true, ephemeral: true,
components: [], components: []
}); });
}); });
} catch (err) { } catch (err) {

View file

@ -16,28 +16,43 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
EmbedBuilder,
Events,
Interaction,
ModalSubmitInteraction,
TextChannel
} from "discord.js";
import { BotClient, BOT_TOKEN, deployCommands } from "./bot"; import { BotClient, BOT_TOKEN, deployCommands } from "./bot";
import { commands } from "./commands"; import { commands } from "./commands";
import { StoreMan } from "./storeman"; import { StoreMan } from "./storeman";
import Logger from "./utils/Logger"; import Logger from "./utils/Logger";
import getRandomColor from "./utils/getRandomColor";
import { submit } from "./modals";
export const dt = new StoreMan(StoreMan.checkFile()); export const dt = new StoreMan(StoreMan.checkFile());
const logger = new Logger("Main"); const logger = new Logger("Main");
BotClient.once("ready", (client) => { BotClient.once("ready", client => {
logger.log(`We're ready! Logged in as ${client.user.tag}`); logger.log(`We're ready! Logged in as ${client.user.tag}`);
}); });
BotClient.on("guildCreate", async (guild) => { // Deploy the commands for a new guild
BotClient.on("guildCreate", async guild => {
await deployCommands({ guildId: guild.id }); await deployCommands({ guildId: guild.id });
}); });
BotClient.on("guildDelete", (guild) => { // Delete the data for a guild after it is removed
BotClient.on("guildDelete", guild => {
logger.log(`${guild.name} didn't want us anymore... :(`); logger.log(`${guild.name} didn't want us anymore... :(`);
dt.clearSettings(guild.id); dt.clearSettings(guild.id);
}); });
BotClient.on("interactionCreate", async (interaction) => { BotClient.on(Events.InteractionCreate, async interaction => {
if (!interaction.isCommand()) { if (!interaction.isCommand()) {
return; return;
} }
@ -49,4 +64,145 @@ BotClient.on("interactionCreate", async (interaction) => {
} }
}); });
BotClient.on(Events.MessageDelete, async message => {
const id = message.guild?.id!;
const confessions = dt.getGuildInfo(id)?.confessions!;
for (const confession of confessions) {
if (confession.messageId === id) {
dt.adminDelConfession(id, confession.id);
}
}
});
BotClient.on(Events.InteractionCreate, async interaction => {
if (!interaction.isModalSubmit()) {
return;
}
if (interaction.customId === "submitConfession") {
const messageContent: string =
interaction.fields.getTextInputValue("confessionInput");
const attachment: string = interaction.fields.getTextInputValue(
"confessionAttachment"
);
try {
if (dt.isBanned(interaction.guild?.id!, interaction.user.id)) {
return interaction.reply({
content: "You are banned from confessions in this server!",
ephemeral: true
});
}
if (!dt.getGuildInfo(interaction.guild?.id!)) {
return interaction.reply({
content:
"The bot hasn't been set up yet! Ask the server admins to set it up.",
ephemeral: true
});
}
const confessChannel = dt.getGuildInfo(interaction.guild?.id!)?.settings
.confessChannel;
const adminChannel = dt.getGuildInfo(interaction.guild?.id!)?.settings
.modChannel;
const isAttachment = (text: string) =>
text && (text.startsWith("http://") || text.startsWith("https://"));
const color = getRandomColor();
const messageId = StoreMan.genId();
const userConfessionEmbed = new EmbedBuilder()
.setColor(color)
.setTitle(`Anonymous Confession \`${messageId}\``)
// @ts-ignore
.setDescription(messageContent);
isAttachment(attachment) && userConfessionEmbed.setImage(attachment);
const adminConfessionEmbed = new EmbedBuilder()
.setColor(color)
.setTitle(`Anonymous Confession \`${messageId}\``)
// @ts-ignore
.setDescription(messageContent)
.addFields(
{
name: "Author",
value: interaction.user.displayName
},
{
name: "Author ID",
value: interaction.user.id
}
);
isAttachment(attachment) && adminConfessionEmbed.setImage(attachment);
const submitConfessionButton = new ButtonBuilder()
.setCustomId("submitConfession")
.setLabel("Submit a Confession")
.setStyle(ButtonStyle.Primary);
const actionRow = new ActionRowBuilder<ButtonBuilder>().setComponents(
submitConfessionButton
);
const message = await (
BotClient.channels.cache.get(confessChannel!) as TextChannel
).send({
embeds: [userConfessionEmbed],
components: [actionRow]
});
const collector = message.createMessageComponentCollector({
componentType: ComponentType.Button
});
collector.on("collect", i => {
if (i.customId === "submitConfession") {
i.showModal(submit);
}
});
await (BotClient.channels.cache.get(adminChannel!) as TextChannel).send({
embeds: [adminConfessionEmbed]
});
dt.addConfession(
message,
messageId,
interaction.user.displayName,
interaction.user.id,
messageContent,
attachment
);
const confessionsLength = dt.getGuildInfo(interaction.guild?.id!)
?.confessions.length!;
if (confessionsLength >= 2) {
await (
BotClient.channels.cache.get(confessChannel!) as TextChannel
).messages
.fetch(
dt.getGuildInfo(interaction.guild?.id!)?.confessions[
confessionsLength - 2
].messageId!
)
.then(message => {
message.edit({ components: [] });
});
}
return interaction.reply({
content: "Confession sent!",
ephemeral: true
});
} catch (err) {
logger.error("An error occured:", err);
}
}
});
BotClient.login(BOT_TOKEN); BotClient.login(BOT_TOKEN);

19
src/modals/index.ts Normal file
View file

@ -0,0 +1,19 @@
/*
* Confoss: Anonymous confessions for Discord, free as in freedom and price!
* Copyright (C) 2024 powermaker450
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./submit";

56
src/modals/submit.ts Normal file
View file

@ -0,0 +1,56 @@
/*
* Confoss: Anonymous confessions for Discord, free as in freedom and price!
* Copyright (C) 2024 powermaker450
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
ActionRowBuilder,
ModalActionRowComponentBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle
} from "discord.js";
const submit = new ModalBuilder()
.setCustomId("submitConfession")
.setTitle("Submit Confession");
const confessionInput = new TextInputBuilder()
.setCustomId("confessionInput")
.setLabel("Confession")
.setRequired(true)
.setMaxLength(2000)
.setStyle(TextInputStyle.Paragraph);
const attachmentInput = new TextInputBuilder()
.setCustomId("confessionAttachment")
.setLabel("Attachment (optional)")
.setRequired(false)
.setStyle(TextInputStyle.Short);
const confessionRow =
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
confessionInput
);
const attachmentRow =
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
attachmentInput
);
submit.addComponents(confessionRow, attachmentRow);
export { submit };

View file

@ -18,7 +18,7 @@
import fs from "fs"; import fs from "fs";
import crypto from "crypto"; import crypto from "crypto";
import { Confession, GuildData, GuildSettings } from "./types"; import { Confession, ConfessionBan, GuildData, GuildSettings } from "./types";
import { DATA_DIR } from "./config"; import { DATA_DIR } from "./config";
import { CommandInteraction, Message } from "discord.js"; import { CommandInteraction, Message } from "discord.js";
@ -39,6 +39,7 @@ export class StoreMan {
author: string, author: string,
authorId: string, authorId: string,
content: string, content: string,
attachment?: string
): Confession { ): Confession {
return { return {
id: id, id: id,
@ -46,6 +47,7 @@ export class StoreMan {
author: author, author: author,
authorId: authorId, authorId: authorId,
content: content, content: content,
attachment: attachment
}; };
} }
@ -72,7 +74,7 @@ export class StoreMan {
fs.writeFileSync( fs.writeFileSync(
StoreMan.fullPath, StoreMan.fullPath,
JSON.stringify(this.data, null, 2), JSON.stringify(this.data, null, 2),
"utf8", "utf8"
); );
} }
@ -92,7 +94,7 @@ export class StoreMan {
this.data.push({ this.data.push({
id: id, id: id,
confessions: [], confessions: [],
settings: opts, settings: opts
}); });
this.saveFile(); this.saveFile();
@ -100,7 +102,7 @@ export class StoreMan {
// Clear the settings for a given guild // Clear the settings for a given guild
public clearSettings(id: string): void { public clearSettings(id: string): void {
this.data = this.data.filter((guild) => { this.data = this.data.filter(guild => {
return guild.id !== id; return guild.id !== id;
}); });
this.saveFile(); this.saveFile();
@ -123,6 +125,7 @@ export class StoreMan {
author: string, author: string,
authorId: string, authorId: string,
content: string, content: string,
attachment?: string
): boolean { ): boolean {
const guildId = message.guild?.id; const guildId = message.guild?.id;
@ -134,7 +137,14 @@ export class StoreMan {
} }
guild.confessions.push( guild.confessions.push(
StoreMan.toConfession(message, id, author, authorId, content), StoreMan.toConfession(
message,
id,
author,
authorId,
content,
attachment
)
); );
this.saveFile(); this.saveFile();
return true; return true;
@ -142,13 +152,13 @@ export class StoreMan {
} }
throw new Error( throw new Error(
`No guild with id ${id} was found. Something's pretty wrong.`, `No guild with id ${id} was found. Something's pretty wrong.`
); );
} }
public getConfession( public getConfession(
guildId: string, guildId: string,
confessionId: string, confessionId: string
): Confession | null { ): Confession | null {
for (const guild of this.data) { for (const guild of this.data) {
if (guild.id === guildId) { if (guild.id === guildId) {
@ -166,7 +176,7 @@ export class StoreMan {
// Attempts to delete a confession. If it is sucessfully deleted, returns true, else false. // Attempts to delete a confession. If it is sucessfully deleted, returns true, else false.
public delConfesssion( public delConfesssion(
{ guild, user }: CommandInteraction, { guild, user }: CommandInteraction,
confessionId: string, confessionId: string
): boolean { ): boolean {
const guildId = guild?.id; const guildId = guild?.id;
const userId = user.id; const userId = user.id;
@ -175,7 +185,7 @@ export class StoreMan {
if (guild.id === guildId) { if (guild.id === guildId) {
for (const confession of guild.confessions) { for (const confession of guild.confessions) {
if (confession.authorId === userId) { if (confession.authorId === userId) {
guild.confessions = guild.confessions.filter((confession) => { guild.confessions = guild.confessions.filter(confession => {
return confession.id !== confessionId; return confession.id !== confessionId;
}); });
@ -189,12 +199,22 @@ export class StoreMan {
return false; return false;
} }
public adminDelConfession(guildId: string, confessionId: string): void {
for (const guild of this.data) {
if (guild.id === guildId) {
guild.confessions = guild.confessions.filter(confession => {
return confession.id !== confessionId;
});
}
}
}
// Check if a certain user is banned within a guild. // Check if a certain user is banned within a guild.
public isBanned(guildId: string, userId: string): boolean { public isBanned(guildId: string, userId: string): boolean {
for (const guild of this.data) { for (const guild of this.data) {
if (guild.id === guildId) { if (guild.id === guildId) {
for (const ban of guild.settings.bans) { for (const ban of guild.settings.bans) {
if (ban === userId) { if (ban.user === userId) {
return true; return true;
} }
} }
@ -204,7 +224,7 @@ export class StoreMan {
return false; return false;
} }
public getBans(guildId: string): string[] { public getBans(guildId: string): ConfessionBan[] {
for (const guild of this.data) { for (const guild of this.data) {
if (guild.id === guildId) { if (guild.id === guildId) {
return guild.settings.bans; return guild.settings.bans;
@ -223,7 +243,10 @@ export class StoreMan {
if (confession) { if (confession) {
// Only add the user to the ban list if they aren't banned already // Only add the user to the ban list if they aren't banned already
!this.isBanned(guildId, confession.authorId) && !this.isBanned(guildId, confession.authorId) &&
guild.settings.bans.push(confession.authorId!); guild.settings.bans.push({
user: confession.authorId,
confessionId: confessionId
});
this.saveFile(); this.saveFile();
return true; return true;
@ -239,8 +262,10 @@ export class StoreMan {
for (const guild of this.data) { for (const guild of this.data) {
if (guild.id === guildId) { if (guild.id === guildId) {
if (this.getConfession(guildId, confessionId)) { if (this.getConfession(guildId, confessionId)) {
guild.settings.bans = guild.settings.bans.filter((ban) => { guild.settings.bans = guild.settings.bans.filter(ban => {
return ban !== this.getConfession(guildId, confessionId)?.authorId!; return (
ban.user !== this.getConfession(guildId, confessionId)?.authorId!
);
}); });
this.saveFile(); this.saveFile();

View file

@ -22,12 +22,18 @@ export interface Confession {
author: string; author: string;
authorId: string; authorId: string;
content: string; content: string;
attachment?: string;
}
export interface ConfessionBan {
user: string;
confessionId: string;
} }
export interface GuildSettings { export interface GuildSettings {
confessChannel: string; confessChannel: string;
modChannel: string; modChannel: string;
bans: string[]; bans: ConfessionBan[];
} }
export interface GuildData { export interface GuildData {

View file

@ -36,7 +36,7 @@ export default class Logger {
public static readonly udln = chalk.underline; public static readonly udln = chalk.underline;
public static readonly anon = Logger.bold.gray( public static readonly anon = Logger.bold.gray(
`[ConfessBot] | ${Logger.emp("Anonymous ")}`, `[ConfessBot] | ${Logger.emp("Anonymous ")}`
); );
constructor(origin?: string) { constructor(origin?: string) {