From a26fb2df1251a848cf1dcdd4452d2eea9be4e6b2 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 15:36:33 +0100 Subject: [PATCH] /blocklist --- README.md | 10 ++ package.json | 2 +- src/commands/blocklist.ts | 194 ++++++++++++++++++++++ src/commands/ci.ts | 3 +- src/commands/index.ts | 2 + src/commands/notification.ts | 30 +--- src/data/migrations/1669300160536-data.ts | 26 +++ src/data/model/guildSetting.ts | 2 +- src/tools/data.ts | 29 ++++ src/tools/defaultEmbeds.ts | 7 + src/tools/discord.ts | 10 ++ 11 files changed, 285 insertions(+), 30 deletions(-) create mode 100644 src/commands/blocklist.ts create mode 100644 src/data/migrations/1669300160536-data.ts create mode 100644 src/tools/data.ts create mode 100644 src/tools/discord.ts diff --git a/README.md b/README.md index 717bc17..5e0825b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,16 @@ A Discord bot that checks Discord channel names for banned words and prevents re ### /logchanel [channel?] Sets the channel where the bot will log if a channel meets the banned word criteria. If channel is omitted, the log channel will be disabled. +### /blocklist +#### /blocklist get +Gets the global and server specific banned word list and returns it + +#### /blocklist add [word] +Adds the word to the server specific blocklist + +#### /blocklist remove [word] +Removes the word from the server specific blocklist + ## Environment variables | Name | Description | Required | Example | | :---------- | :------------------------------------------------------------ | :------: | :------------------ | diff --git a/package.json b/package.json index 1be8d2e..ec74e5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "description": "A Discord bot that checks channel names for blacklisted words and reverts the changes if necessary", "main": "build/index.js", "scripts": { diff --git a/src/commands/blocklist.ts b/src/commands/blocklist.ts new file mode 100644 index 0000000..ba99b95 --- /dev/null +++ b/src/commands/blocklist.ts @@ -0,0 +1,194 @@ +import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { database, Badword } from "../data"; +import { IsNull } from "typeorm"; +import { getGuildSetting, isPremiumActive } from "../tools/data"; +import getDefaultEmbed, { getFailedEmbed, getSuccessEmbed } from "../tools/defaultEmbeds"; +import { getGuildChannel } from "../tools/discord"; + +const builder = new SlashCommandBuilder(); +builder.setName("blocklist"); +builder.setDescription("Configures the servers blocklist"); +builder.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild); +builder.setDMPermission(false); +builder.addSubcommand((builder) => { + builder.setName("add"); + builder.setDescription("Adds a word to the servers blocklist"); + builder.addStringOption((option) => { + option.setName("word"); + option.setDescription("The word to add"); + option.setRequired(true); + option.setMaxLength(50); + return option; + }); + return builder; +}); +builder.addSubcommand((builder) => { + builder.setName("remove"); + builder.setDescription("Removes a word from the servers blocklist"); + builder.addStringOption((option) => { + option.setName("word"); + option.setDescription("The word to remove"); + option.setRequired(true); + option.setMaxLength(50); + return option; + }); + return builder; +}); +builder.addSubcommand((builder) => { + builder.setName("get"); + builder.setDescription("Returns all words from the servers blocklist"); + return builder; +}); + +async function execute(interaction: ChatInputCommandInteraction): Promise { + if (!interaction.guildId) throw new Error("Command was executed in DM context"); + + const settings = await getGuildSetting(interaction.guildId); + const isPremium = isPremiumActive(settings.isPremiumUntil); + + const logChannel = settings.notificationChannelID ? await getGuildChannel(interaction.guildId, settings.notificationChannelID) : null; + + switch (interaction.options.getSubcommand(true)) { + case "get": { + const guildBadWords = await database.getRepository(Badword).find({ + select: { + value: true + }, + where: { + guildID: interaction.guildId + } + }); + const globalBadWords = await database.getRepository(Badword).find({ + where: { + guildID: IsNull() + } + }); + + interaction.reply({ + content: `\`\`\`Global bad word list\`\`\`\n||${globalBadWords.map((word) => word.value).reduce((prev, next) => prev + ", " + next, "").slice(2)} ||\n\`\`\`Local server bad word list (${guildBadWords.length}/${isPremium ? 100 : 10})\`\`\`\n||${guildBadWords.map((word) => word.value).reduce((prev, next) => prev + ", " + next, "").slice(2)} ||`, + ephemeral: true + }).catch(); + break; + } + + case "add": { + const count = await database.getRepository(Badword).count({ + where: { + guildID: interaction.guildId + } + }); + + const limit = isPremium ? 100 : 10; + if (count >= limit) { + const embed = getFailedEmbed(); + embed.setDescription("You reached the word limit for your guild. Please delete a word before adding a new one"); + interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); + return; + } + + const word = interaction.options.getString("word", true).toLowerCase(); + + const exists = await database.getRepository(Badword).count({ + where: [ + {guildID: interaction.guildId, value: word}, + {guildID: IsNull(), value: word} + ] + }) > 0; + + if (exists) { + const embed = getFailedEmbed(); + embed.setDescription(`"${word}" already exists in the blocklist`); + interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); + return; + } + + const entry = new Badword(); + entry.guildID = interaction.guildId; + entry.value = word; + + await database.getRepository(Badword).save(entry); + + const embed = getSuccessEmbed(); + embed.setDescription(`"${word}" has been added to the blocklist`); + interaction.reply({ + embeds: [embed], + ephemeral: true + }); + + const logMessage = getDefaultEmbed(); + logMessage.setTitle("Word added"); + logMessage.setDescription(`"||${word}||" has been added to the blocklist`); + logMessage.addFields({ + name: "This action was performed by", + value: `${interaction.user.tag} (${interaction.user.id})` + }); + if (logChannel && logChannel.isTextBased()) { + logChannel.send({ + embeds: [logMessage] + }).catch(); + } + break; + } + + case "remove": { + const word = interaction.options.getString("word", true).toLowerCase(); + + const entry = await database.getRepository(Badword).findOne({ + where: { + guildID: interaction.guildId, + value: word + } + }); + + if (!entry) { + const embed = getFailedEmbed(); + embed.setDescription(`"${word}" was not found in the blocklist`); + interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); + return; + } + + await database.getRepository(Badword).delete({ + id: entry.id + }); + + const embed = getSuccessEmbed(); + embed.setDescription(`"${word}" has been removed from the blocklist`); + interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); + + const logMessage = getDefaultEmbed(); + logMessage.setTitle("Word removed"); + logMessage.setDescription(`"||${word}||" has been removed from the blocklist`); + logMessage.addFields({ + name: "This action was performed by", + value: `${interaction.user.tag} (${interaction.user.id})` + }); + if (logChannel && logChannel.isTextBased()) { + logChannel.send({ + embeds: [logMessage] + }).catch(); + } + break; + } + + default: { + throw new Error(`"${interaction.options.getSubcommand(true)}" cannot be executed`); + } + } +} + +export { + builder, + execute +} \ No newline at end of file diff --git a/src/commands/ci.ts b/src/commands/ci.ts index aee9a24..2216d4b 100644 --- a/src/commands/ci.ts +++ b/src/commands/ci.ts @@ -1,6 +1,7 @@ import * as notification from "./notification"; +import * as blocklist from "./blocklist"; -const array = [notification.builder.toJSON()]; +const array = [notification.builder.toJSON(), blocklist.builder.toJSON()]; export { array diff --git a/src/commands/index.ts b/src/commands/index.ts index e34d322..b2a3908 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,10 +1,12 @@ import { ChatInputCommandInteraction, Collection, Events, SlashCommandBuilder } from "discord.js"; import * as notification from "./notification"; +import * as blocklist from "./blocklist"; import client from "../client"; import getDefaultEmbed from "../tools/defaultEmbeds"; const commands = new Collection Promise }>(); commands.set(notification.builder.name, notification); +commands.set(blocklist.builder.name, blocklist); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; diff --git a/src/commands/notification.ts b/src/commands/notification.ts index 49bfa48..4b4c2a5 100644 --- a/src/commands/notification.ts +++ b/src/commands/notification.ts @@ -1,7 +1,8 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ChatInputCommandInteraction, NewsChannel, TextBasedChannel, CategoryChannel, StageChannel, TextChannel, PrivateThreadChannel, PublicThreadChannel, VoiceChannel, APIInteractionDataResolvedChannel, ForumChannel, GuildBasedChannel } from "discord.js"; +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ChatInputCommandInteraction, NewsChannel, TextBasedChannel, CategoryChannel, StageChannel, TextChannel, PrivateThreadChannel, PublicThreadChannel, VoiceChannel, APIInteractionDataResolvedChannel, ForumChannel } from "discord.js"; import getDefaultEmbed, { getSuccessEmbed } from "../tools/defaultEmbeds"; import { database, GuildSetting } from "../data"; -import client from "../client"; +import { getGuildSetting } from "../tools/data"; +import { getGuildChannel } from "../tools/discord"; const builder = new SlashCommandBuilder(); builder.setName("logchannel"); @@ -16,31 +17,6 @@ builder.addChannelOption((option) => { return option; }); -async function getGuildSetting(guildID: string): Promise { - let guildSetting = await database.getRepository(GuildSetting).findOne({ - where: { - id: guildID - } - }); - - if (!guildSetting) { - guildSetting = new GuildSetting(); - guildSetting.id = guildID; - guildSetting.isPremiumUntil = null; - guildSetting.notificationChannelID = null; - } - - return guildSetting; -} - -async function getGuildChannel(guildID: string, channelID: string): Promise { - const guild = await client.guilds.fetch(guildID); - if (!guild) return null; - - const channel = await guild.channels.fetch(channelID); - return channel; -} - async function resetNotificationChannel(guildSetting: GuildSetting, interaction: ChatInputCommandInteraction): Promise { const logChannel = guildSetting.notificationChannelID ? await getGuildChannel(guildSetting.id, guildSetting.notificationChannelID) : null; diff --git a/src/data/migrations/1669300160536-data.ts b/src/data/migrations/1669300160536-data.ts new file mode 100644 index 0000000..df7074e --- /dev/null +++ b/src/data/migrations/1669300160536-data.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class data1669300160536 implements MigrationInterface { + name = 'data1669300160536' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "guild_setting" DROP COLUMN "isPremiumUntil" + `); + await queryRunner.query(` + ALTER TABLE "guild_setting" + ADD "isPremiumUntil" TIMESTAMP + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "guild_setting" DROP COLUMN "isPremiumUntil" + `); + await queryRunner.query(` + ALTER TABLE "guild_setting" + ADD "isPremiumUntil" date + `); + } + +} diff --git a/src/data/model/guildSetting.ts b/src/data/model/guildSetting.ts index 9ccca0f..45c83ad 100644 --- a/src/data/model/guildSetting.ts +++ b/src/data/model/guildSetting.ts @@ -8,6 +8,6 @@ export class GuildSetting { @Column("varchar", { nullable: true, default: null }) notificationChannelID!: string | null; - @Column("date", { nullable: true, default: null }) + @Column("timestamp", { nullable: true, default: null }) isPremiumUntil!: Date | null; } \ No newline at end of file diff --git a/src/tools/data.ts b/src/tools/data.ts new file mode 100644 index 0000000..11b5b30 --- /dev/null +++ b/src/tools/data.ts @@ -0,0 +1,29 @@ +import { database, GuildSetting } from "../data"; + +export async function getGuildSetting(guildID: string): Promise { + let guildSetting = await database.getRepository(GuildSetting).findOne({ + where: { + id: guildID + } + }); + + if (!guildSetting) { + guildSetting = new GuildSetting(); + guildSetting.id = guildID; + guildSetting.isPremiumUntil = null; + guildSetting.notificationChannelID = null; + } + + return guildSetting; +} + +export function isPremiumActive(timestamp: Date | null): boolean { + + console.log(timestamp); + + if (timestamp === null) return false; + const now = Number(new Date()); + const activeUntil = Number(timestamp); + + return now < activeUntil; +} \ No newline at end of file diff --git a/src/tools/defaultEmbeds.ts b/src/tools/defaultEmbeds.ts index 4338fe4..7004af2 100644 --- a/src/tools/defaultEmbeds.ts +++ b/src/tools/defaultEmbeds.ts @@ -21,4 +21,11 @@ export function getSuccessEmbed(): EmbedBuilder { embed.setTitle("Success"); embed.setColor(0x32d122); return embed; +} + +export function getFailedEmbed(): EmbedBuilder { + const embed = getDefaultEmbed(); + embed.setTitle("Failed"); + embed.setColor(0xD01B15); + return embed; } \ No newline at end of file diff --git a/src/tools/discord.ts b/src/tools/discord.ts new file mode 100644 index 0000000..baa2d0f --- /dev/null +++ b/src/tools/discord.ts @@ -0,0 +1,10 @@ +import { GuildBasedChannel } from "discord.js"; +import client from "../client"; + +export async function getGuildChannel(guildID: string, channelID: string): Promise { + const guild = await client.guilds.fetch(guildID); + if (!guild) return null; + + const channel = await guild.channels.fetch(channelID); + return channel; +} \ No newline at end of file