Compare commits

..

5 commits

Author SHA1 Message Date
powermaker450 e24b163a34 Separate user and confession bans 2024-10-21 12:37:14 -04:00
powermaker450 4564cfb6a2 Cleanup involving destructures and prettier 2024-10-20 15:16:49 -04:00
powermaker450 aec01905b5 Differentiate between checking if a user is banned by user or id 2024-10-20 14:26:45 -04:00
powermaker450 7fbc7f4308 Add update command to update slash commands 2024-10-20 10:50:43 -04:00
powermaker450 e7bc0a32f6 Don't react to message deletes if the guild hasn't been set up yet 2024-10-18 15:25:40 -04:00
9 changed files with 252 additions and 86 deletions

View file

@ -52,9 +52,12 @@ export const data = new SlashCommandBuilder()
export async function execute(interaction: ChatInputCommandInteraction) {
// TODO: This all works as intended, but I'd like for it so be a reusable function
// instead because all of this is used in src/main.ts
const { id: guildId } = interaction.guild!;
const { id: userId } = interaction.user;
try {
// If the user is banned in this guild, don't let them post
if (dt.isBanned(interaction.guild?.id!, interaction.user.id)) {
if (dt.isBannedByUser(guildId, userId)) {
return interaction.reply({
content: "You are banned from confessions in this server!",
ephemeral: true
@ -62,7 +65,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
}
// If no guild info is present for this guild, don't let the user post
if (!dt.getGuildInfo(interaction.guild?.id!)) {
if (!dt.getGuildInfo(guildId)) {
return interaction.reply({
content:
"The bot hasn't been set up yet! Ask the server admins to set it up.",
@ -70,10 +73,8 @@ export async function execute(interaction: ChatInputCommandInteraction) {
});
}
const confessChannel = dt.getGuildInfo(interaction.guild?.id!)?.settings
.confessChannel;
const adminChannel = dt.getGuildInfo(interaction.guild?.id!)?.settings
.modChannel;
const confessChannel = dt.getGuildInfo(guildId)?.settings.confessChannel;
const adminChannel = dt.getGuildInfo(guildId)?.settings.modChannel;
const messageContent = `"${interaction.options.getString("message")}"`;
const attachment = interaction.options.getString("attachment")!;
@ -121,11 +122,11 @@ export async function execute(interaction: ChatInputCommandInteraction) {
.addFields(
{
name: "Author",
value: `<@${interaction.user.id}>`
value: `<@${userId}>`
},
{
name: "Author ID",
value: interaction.user.id
value: userId
}
);
@ -153,9 +154,10 @@ export async function execute(interaction: ChatInputCommandInteraction) {
components: [actionRow]
});
adminChannel && await (BotClient.channels.cache.get(adminChannel!) as TextChannel).send({
embeds: [adminConfessionEmbed]
});
adminChannel &&
(await (BotClient.channels.cache.get(adminChannel!) as TextChannel).send({
embeds: [adminConfessionEmbed]
}));
dt.addConfession(
message,
@ -166,8 +168,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
attachment
);
const confessionsLength = dt.getGuildInfo(interaction.guild?.id!)
?.confessions.length!;
const confessionsLength = dt.getGuildInfo(guildId)!.confessions.length;
// If there are 2 or more confessions, remove the previous confession's button components
if (confessionsLength >= 2) {
@ -175,9 +176,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
BotClient.channels.cache.get(confessChannel!) as TextChannel
).messages
.fetch(
dt.getGuildInfo(interaction.guild?.id!)?.confessions[
confessionsLength - 2
].messageId!
dt.getGuildInfo(guildId)!.confessions[confessionsLength - 2].messageId
)
.then(message => {
message.edit({ components: [] });

View file

@ -38,8 +38,11 @@ export const data = new SlashCommandBuilder()
);
export async function execute(interaction: ChatInputCommandInteraction) {
const { id: guildId } = interaction.guild!;
const { id: userId } = interaction.user;
// If there is no guild info, don't let the user delete anything
if (!dt.getGuildInfo(interaction.guild?.id!)) {
if (!dt.getGuildInfo(guildId)) {
return interaction.reply({
content:
"The bot hasn't been set up yet! Ask the server admins to set it up.",
@ -48,9 +51,9 @@ export async function execute(interaction: ChatInputCommandInteraction) {
}
const idVal = interaction.options.getString("id")!;
const result = dt.getConfession(interaction.guild?.id!, idVal);
const result = dt.getConfession(guildId, idVal);
// If there is a result, and the user is either an author or has manage messages
const allowedByUser = result && result.authorId === interaction.user.id;
const allowedByUser = result && result.authorId === userId;
const allowedByMod =
result &&
interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages);
@ -58,12 +61,8 @@ export async function execute(interaction: ChatInputCommandInteraction) {
// If a confession is found with the given ID, check if the user is the one that posted it, and delete it if they are.
// Otherwise, don't let the user delete anything.
if (allowedByUser || allowedByMod) {
const confession = dt.getConfession(
interaction.guild?.id!,
idVal
)?.messageId;
const channelId = dt.getGuildInfo(interaction.guild?.id!)?.settings
.confessChannel!;
const confession = dt.getConfession(guildId, idVal)!.messageId;
const channelId = dt.getGuildInfo(guildId)!.settings.confessChannel;
const emptyEmbed = new EmbedBuilder()
.setColor(getRandomColor())
.setTitle("Confession Deleted")
@ -76,7 +75,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
try {
// Replace the given confession with an empty embed
await (BotClient.channels.cache.get(channelId) as TextChannel).messages
.fetch(confession!)
.fetch(confession)
.then(e => {
e.edit({
embeds: [emptyEmbed]

View file

@ -18,11 +18,16 @@
import {
ChatInputCommandInteraction,
heading,
HeadingLevel,
inlineCode,
italic,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { dt } from "../main";
import Logger from "../utils/Logger";
import { BanReason } from "../storeman";
const logger = new Logger("(/) confessban");
@ -33,7 +38,7 @@ export const data = new SlashCommandBuilder()
.addSubcommand(ban =>
ban
.setName("ban")
.setDescription("Ban a user from confessions")
.setDescription("Ban an ID from confessions")
.addStringOption(option =>
option
.setName("id")
@ -43,6 +48,14 @@ export const data = new SlashCommandBuilder()
.setRequired(true)
)
)
.addSubcommand(banuser =>
banuser
.setName("banuser")
.setDescription("Ban a user from confessions")
.addUserOption(user =>
user.setName("user").setDescription("The user to ban").setRequired(true)
)
)
.addSubcommand(list =>
list.setName("list").setDescription("Show the list of banned users")
)
@ -61,13 +74,13 @@ export const data = new SlashCommandBuilder()
);
export async function execute(interaction: ChatInputCommandInteraction) {
const guildId = interaction.guild?.id!;
const { id: guildId } = interaction.guild!;
// /confessmod ban <id>
if (interaction.options.getSubcommand() === "ban") {
const confessionId = interaction.options.getString("id")!;
if (dt.isBanned(guildId, confessionId)) {
if (dt.isBannedById(guildId, confessionId)) {
try {
return interaction.reply({
content: "That user is already banned!",
@ -78,7 +91,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
}
}
const result = dt.addBan(guildId, confessionId);
const result = dt.addBanById(guildId, confessionId);
try {
return result
@ -93,41 +106,76 @@ export async function execute(interaction: ChatInputCommandInteraction) {
} catch (err) {
logger.error("A ban interaction error occured:", err);
}
// /confessmod banuser <user>
} else if (interaction.options.getSubcommand() === "banuser") {
const { id: userId } = interaction.options.getUser("user")!;
const result = dt.addBanByUser(guildId, userId);
try {
return result
? interaction.reply({
content: "User was banned.",
ephemeral: true
})
: interaction.reply({
content: "How did we get here? (An error occured.)}",
ephemeral: true
});
} catch (err) {
logger.error("A banuser interaction error occured:", err);
}
// /confessmod list
} else if (interaction.options.getSubcommand() === "list") {
const bannedMembers = dt.getBans(interaction.guild?.id!);
const bannedMembers = dt.getBans(guildId);
let content = bannedMembers.length
? "Banned Members:\n"
: "There are no banned members.";
const determineContent = () => {
if (!bannedMembers.length) {
return "There are no bans.";
}
// For each member, add them to the message content.
// It will end up looking something like this:
//
// Banned Members:
//
// @user1 | a1b2
// @user2 | c3d4
// @user3 | e5f6
//
for (const member of bannedMembers) {
content += `\n<@${member.user}> | \`${member.confessionId}\``;
let userHead = heading("Users:", HeadingLevel.Two);
let userCount = false;
let idHead = "\n" + heading("Confessions:", HeadingLevel.Two);
let idCount = false;
for (const member of bannedMembers) {
if (member.method === BanReason.ByUser) {
userHead += "\n" + `<@${member.user}>`;
userCount = true;
} else if (member.method === BanReason.ById) {
const confession = dt.getConfession(guildId, member.confessionId!)!;
idHead += `\nConfession ${inlineCode(member.confessionId!)}: ${italic(confession.content)}`;
idCount = true;
}
}
// If there are users and confessions use both headers, otherwise use whichever is populated
if (userCount && idCount) {
return userHead + idHead;
} else {
return userCount
? userHead
: idHead;
}
}
try {
return interaction.reply({
content: content,
content: determineContent(),
ephemeral: true
});
} catch (err) {
logger.error("A banlist interaction error occured:", err);
return interaction.reply({
content: "A server-side error occurred when getting the ban list.",
ephemeral: true
});
}
// /confessmod pardon <id>
} else if (interaction.options.getSubcommand() === "pardon") {
const result = dt.removeBan(
interaction.guild?.id!,
interaction.options.getString("id")!
);
const result = dt.removeBan(guildId, interaction.options.getString("id")!);
try {
return result

View file

@ -21,11 +21,13 @@ import * as confessdel from "./confessdel";
import * as confessmod from "./confessmod";
import * as ping from "./ping";
import * as setup from "./setup";
import * as update from "./update";
export const commands = {
confess,
confessdel,
confessmod,
ping,
setup
setup,
update
};

View file

@ -38,14 +38,16 @@ export const data = new SlashCommandBuilder()
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
export async function execute(interaction: CommandInteraction) {
if (dt.checkSetup(interaction.guild?.id!)) {
const { id: guildId } = interaction.guild!;
const { displayName: username } = interaction.user;
if (dt.checkSetup(guildId)) {
return interaction.reply({
content: "This guild has already been set up!",
ephemeral: true
});
}
const guildId = interaction.guild?.id;
let confessChannel: string, logChannel: string;
const channelList = new ChannelSelectMenuBuilder()
@ -56,17 +58,18 @@ export async function execute(interaction: CommandInteraction) {
const skipButton = new ButtonBuilder()
.setCustomId("skipModChannel")
.setLabel("Skip")
.setStyle(ButtonStyle.Secondary)
.setStyle(ButtonStyle.Secondary);
const channelRow =
new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(channelList);
const buttonRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents(skipButton);
const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
skipButton
);
try {
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, ${username}!\nFirst, let's choose a channel for your confessions.`,
ephemeral: true,
components: [channelRow]
});
@ -77,7 +80,7 @@ export async function execute(interaction: CommandInteraction) {
});
collector.on("collect", async i => {
[ confessChannel ] = i.values;
[confessChannel] = i.values;
await i.update({
content: "Awesome!",
@ -115,14 +118,14 @@ export async function execute(interaction: CommandInteraction) {
let skipped = false;
logCollector.on("collect", async ij => {
[ logChannel ] = ij.values;
[logChannel] = ij.values;
await ij.update({
content: "Setup Complete!",
components: []
});
dt.setup(guildId!, {
dt.setup(guildId, {
confessChannel: confessChannel,
modChannel: logChannel,
bans: []
@ -148,11 +151,12 @@ export async function execute(interaction: CommandInteraction) {
logCollector.stop();
skipCollector.stop();
}
})
});
logCollector.on("end", content => {
// If there is no content and the channel hasn't been skipped, follow up with an error message.
(!content.size && !skipped) &&
!content.size &&
!skipped &&
interaction.followUp({
content: "No channel selected. Please try again.",
ephemeral: true,

63
src/commands/update.ts Normal file
View file

@ -0,0 +1,63 @@
/*
* 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 {
ChatInputCommandInteraction,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { deployCommands } from "../bot";
import Logger from "../utils/Logger";
const logger = new Logger("(/) update");
const minutes = 5;
const cooldownList = new Set();
export const data = new SlashCommandBuilder()
.setName("update")
.setDescription("Update the bot with new data if available")
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
export async function execute(interaction: ChatInputCommandInteraction) {
const { id: guildId, name: guildName } = interaction.guild!;
if (cooldownList.has(guildId)) {
return interaction.reply({
content: `You can only run the update command once every ${minutes} minutes.`,
ephemeral: true
});
}
deployCommands({ guildId: guildId });
cooldownList.add(guildId);
logger.log(`Applied cooldown to "${guildName}"`);
setTimeout(
() => {
cooldownList.delete(guildId);
logger.log(`Removed cooldown from "${guildName}"`);
},
minutes * 60 * 1000
);
return interaction.reply({
content: "Commands refreshed.",
ephemeral: true
});
}

View file

@ -65,8 +65,12 @@ BotClient.on(Events.InteractionCreate, async interaction => {
});
BotClient.on(Events.MessageDelete, async message => {
const guildId = message.guild?.id!;
if (!dt.getGuildInfo(guildId)) {
return;
}
try {
const guildId = message.guild?.id!;
const messageId = message.id;
const confessions = dt.getGuildInfo(guildId)?.confessions!;
@ -93,7 +97,7 @@ BotClient.on(Events.InteractionCreate, async interaction => {
if (requestSubmit) {
// Check if the user is banned from confessions before showing the modal
dt.isBanned(interaction.guild?.id!, interaction.user.id)
dt.isBannedByUser(interaction.guild?.id!, interaction.user.id)
? interaction.reply({
content: "You are banned from confessions in this server!",
ephemeral: true
@ -114,7 +118,7 @@ BotClient.on(Events.InteractionCreate, async interaction => {
);
try {
if (dt.isBanned(interaction.guild?.id!, interaction.user.id)) {
if (dt.isBannedByUser(interaction.guild?.id!, interaction.user.id)) {
return interaction.reply({
content: "You are banned from confessions in this server!",
ephemeral: true
@ -179,9 +183,12 @@ BotClient.on(Events.InteractionCreate, async interaction => {
components: [actionRow]
});
adminChannel && await (BotClient.channels.cache.get(adminChannel!) as TextChannel).send({
embeds: [adminConfessionEmbed]
});
adminChannel &&
(await (
BotClient.channels.cache.get(adminChannel!) as TextChannel
).send({
embeds: [adminConfessionEmbed]
}));
dt.addConfession(
message,

View file

@ -18,7 +18,13 @@
import fs from "fs";
import crypto from "crypto";
import { Confession, ConfessionBan, GuildData, GuildSettings } from "./types";
import {
BanReason,
Confession,
ConfessionBan,
GuildData,
GuildSettings
} from "./types";
import { DATA_DIR } from "./config";
import { CommandInteraction, Message } from "discord.js";
import Logger from "../utils/Logger";
@ -82,9 +88,9 @@ export class StoreMan {
}
// Checks if a guild is not set up
public checkSetup(id: string): boolean {
public checkSetup(guildId: string): boolean {
for (const guild of this.data) {
if (guild.id === id) {
if (guild.id === guildId) {
return true;
}
}
@ -93,9 +99,9 @@ export class StoreMan {
}
// Sets up a guild and stores it in the persistent file
public setup(id: string, opts: GuildSettings): void {
public setup(guildId: string, opts: GuildSettings): void {
this.data.push({
id: id,
id: guildId,
confessions: [],
settings: opts
});
@ -104,16 +110,16 @@ export class StoreMan {
}
// Clear the settings for a given guild
public clearSettings(id: string): void {
public clearSettings(guildId: string): void {
this.data = this.data.filter(guild => {
return guild.id !== id;
return guild.id !== guildId;
});
this.saveFile();
}
public getGuildInfo(id: string): GuildData | null {
public getGuildInfo(guildId: string): GuildData | null {
for (const guild of this.data) {
if (guild.id === id) {
if (guild.id === guildId) {
return guild;
}
}
@ -130,12 +136,12 @@ export class StoreMan {
content: string,
attachment?: string
): boolean {
const guildId = message.guild?.id;
const { id: guildId } = message.guild!;
for (const guild of this.data) {
if (guild.id === guildId) {
// If the author's user ID is in the ban list, don't let them post a confession.
if (this.isBanned(guildId, author)) {
if (this.isBannedByUser(guildId, author)) {
return false;
}
@ -215,7 +221,7 @@ export class StoreMan {
}
// Check if a certain user is banned within a guild.
public isBanned(guildId: string, userId: string): boolean {
public isBannedByUser(guildId: string, userId: string): boolean {
for (const guild of this.data) {
if (guild.id === guildId) {
for (const ban of guild.settings.bans) {
@ -229,6 +235,20 @@ export class StoreMan {
return false;
}
public isBannedById(guildId: string, confessionId: string): boolean {
for (const guild of this.data) {
if (guild.id === guildId) {
for (const ban of guild.settings.bans) {
if (ban.confessionId === confessionId) {
return true;
}
}
}
}
return false;
}
public getBans(guildId: string): ConfessionBan[] {
for (const guild of this.data) {
if (guild.id === guildId) {
@ -240,17 +260,18 @@ export class StoreMan {
}
// Attempts to ban a user from confessions.
public addBan(guildId: string, confessionId: string): boolean {
public addBanById(guildId: string, confessionId: string): boolean {
const confession = this.getConfession(guildId, confessionId);
for (const guild of this.data) {
if (guild.id === guildId) {
if (confession) {
// Only add the user to the ban list if they aren't banned already
!this.isBanned(guildId, confession.authorId) &&
!this.isBannedByUser(guildId, confession.authorId) &&
guild.settings.bans.push({
user: confession.authorId,
confessionId: confessionId
confessionId: confessionId,
method: BanReason.ById
});
this.saveFile();
@ -262,6 +283,23 @@ export class StoreMan {
return false;
}
public addBanByUser(guildId: string, userId: string): boolean {
for (const guild of this.data) {
if (guild.id === guildId) {
// Only add the user to the ban list if they aren't banned already
!this.isBannedByUser(guildId, userId) && guild.settings.bans.push({
user: userId,
method: BanReason.ByUser
});
this.saveFile();
return true;
}
}
return false;
}
// Attempts to pardon a user from a ban. If sucessfully completed, returns true, false if otherwise.
public removeBan(guildId: string, confessionId: string): boolean {
for (const guild of this.data) {

View file

@ -25,9 +25,15 @@ export interface Confession {
attachment?: string;
}
export enum BanReason {
ById,
ByUser
}
export interface ConfessionBan {
user: string;
confessionId: string;
confessionId?: string;
method: BanReason;
}
export interface GuildSettings {