diff --git a/Changelog.md b/Changelog.md index d1929df..7b98e9b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,8 @@ # Changelog This file is used to list changes made to this software. -## V1.0.0 [`unreleased`] -_Current development version: **1.0.0-beta.3**_ +## V1.0.0 [2022-11-29] +_Current development version: **1.0.0**_ ### Features - /info - /logchannel @@ -14,7 +14,9 @@ _Current development version: **1.0.0-beta.3**_ - /showsettings - Data will be deleted by default when the bot leaves the server - Server admins can change the behaviour of the bot when it leaves the server to keep the data persistent -- Scans for blocked words in channel names and renames channels to "CENSORED" if found -- When settings are changed or channels are censored, notifications to a logchannel can be enabled +- Scans for blocked words in channel names and deletes the channel if found +- Gets the user creating or renaming a channel to a blocked word +- When settings are changed or channels are deleted, notifications to a logchannel can be enabled - CLI to change settings and get information during runtime by attaching to the apps docker container -- API for automated uptime checks to prevent the bot from going offline unnoticed \ No newline at end of file +- API for automated uptime checks to prevent the bot from going offline unnoticed +- Bot notifies admins via /showsettings when it lacks permissions needed for its functionality \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e400936..d1f9097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-beta.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-beta.1", + "version": "1.0.0", "license": "CC-BY-NC-ND-4.0", "dependencies": { "discord.js": "^14.6.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "fs-extra": "^10.1.0", + "fs-extra": "^11.0.0", "moment": "^2.29.4", "pg": "^8.8.0", "typeorm": "^0.3.10" @@ -40,42 +40,42 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.3.0.tgz", - "integrity": "sha512-Pvca6Nw8Hp+n3N+Wp17xjygXmMvggbh5ywUsOYE2Et4xkwwVRwgzxDJiMUuYapPtnYt4w/8aKlf5khc8ipLvhg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.4.0.tgz", + "integrity": "sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==", "dependencies": { "@discordjs/util": "^0.1.0", - "@sapphire/shapeshift": "^3.7.0", - "discord-api-types": "^0.37.12", + "@sapphire/shapeshift": "^3.7.1", + "discord-api-types": "^0.37.20", "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.1", - "tslib": "^2.4.0" + "ts-mixer": "^6.0.2", + "tslib": "^2.4.1" }, "engines": { "node": ">=16.9.0" } }, "node_modules/@discordjs/collection": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.2.0.tgz", - "integrity": "sha512-VvrrtGb7vbfPHzbhGq9qZB5o8FOB+kfazrxdt0OtxzSkoBuw9dURMkCwWizZ00+rDpiK2HmLHBZX+y6JsG9khw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.3.0.tgz", + "integrity": "sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==", "engines": { "node": ">=16.9.0" } }, "node_modules/@discordjs/rest": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.3.0.tgz", - "integrity": "sha512-U6X5J+r/MxYpPTlHFuPxXEf92aKsBaD2teBC7sWkKILIr30O8c9+XshfL7KFBCavnAqS/qE+PF9fgRilO3N44g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.4.0.tgz", + "integrity": "sha512-k3Ip7ffFSAfp7Mu4H/3BEXFvFz+JsbXRrRtpeBMnSp1LefhtlZWJE6xdXzNlblktKNQltnRwY+z0NZrGQdxAMw==", "dependencies": { - "@discordjs/collection": "^1.2.0", + "@discordjs/collection": "^1.3.0", "@discordjs/util": "^0.1.0", "@sapphire/async-queue": "^1.5.0", "@sapphire/snowflake": "^3.2.2", - "discord-api-types": "^0.37.12", + "discord-api-types": "^0.37.20", "file-type": "^18.0.0", - "tslib": "^2.4.0", - "undici": "^5.11.0" + "tslib": "^2.4.1", + "undici": "^5.13.0" }, "engines": { "node": ">=16.9.0" @@ -124,9 +124,9 @@ } }, "node_modules/@sapphire/shapeshift": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.7.0.tgz", - "integrity": "sha512-A6vI1zJoxhjWo4grsxpBRBgk96SqSdjLX5WlzKp9H+bJbkM07mvwcbtbVAmUZHbi/OG3HLfiZ1rlw4BhH6tsBQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.7.1.tgz", + "integrity": "sha512-JmYN/0GW49Vl8Hi4PwrsDBNjcuCylH78vWYolVys74LRIzilAAMINxx4RHQOdvYoz+ceJKVp4+zBbQ5kuIFOLw==", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash.uniqwith": "^4.5.0" @@ -402,19 +402,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -641,19 +628,11 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "ms": "2.0.0" } }, "node_modules/depd": { @@ -683,27 +662,27 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.19", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.19.tgz", - "integrity": "sha512-5dVUYJfxCh/RU/S4D7osG3H+Ntl0GBkANO6oVRl35Eo+cd/peoNUAmcSzLzQ9Fqu2RJ9+2vd7DVcqNqYjX38wQ==" + "version": "0.37.20", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.20.tgz", + "integrity": "sha512-uAO+55E11rMkYR36/paE1vKN8c2bZa1mgrIaiQIBgIZRKZTDIGOZB+8I5eMRPFJcGxrg16riUu+0aTu2JQEPew==" }, "node_modules/discord.js": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.6.0.tgz", - "integrity": "sha512-On1K7xpJZRe0KsziIaDih2ksYPhgxym/ZqV45i1f3yig4vUotikqs7qp5oXiTzQ/UTiNRCixUWFTh7vA1YBCqw==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.7.0.tgz", + "integrity": "sha512-CR2JAoqR+82D7mfMZ7toPAqdIk2sMF8wgTc8yDGPPMHzJknIKtkEPtzWFhBYGMZUkK+M4POw08ngBWqK2A4RMg==", "dependencies": { - "@discordjs/builders": "^1.3.0", - "@discordjs/collection": "^1.2.0", - "@discordjs/rest": "^1.3.0", + "@discordjs/builders": "^1.4.0", + "@discordjs/collection": "^1.3.0", + "@discordjs/rest": "^1.4.0", "@discordjs/util": "^0.1.0", "@sapphire/snowflake": "^3.2.2", "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.12", + "discord-api-types": "^0.37.20", "fast-deep-equal": "^3.1.3", "lodash.snakecase": "^4.1.1", - "tslib": "^2.4.0", - "undici": "^5.11.0", - "ws": "^8.9.0" + "tslib": "^2.4.1", + "undici": "^5.13.0", + "ws": "^8.11.0" }, "engines": { "node": ">=16.9.0" @@ -797,19 +776,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -848,19 +814,6 @@ "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -878,16 +831,16 @@ } }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.0.0.tgz", + "integrity": "sha512-4YxRvMi4P5C3WQTvdRfrv5UVqbISpqjORFQAW5QPiKAauaxNCwrEdIi6pG3tDFhKKpMen+enEhHIzB/tvIO+/w==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs.realpath": { @@ -1208,9 +1161,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/mz": { "version": "2.7.0", @@ -1621,19 +1574,6 @@ "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2026,6 +1966,27 @@ } } }, + "node_modules/typeorm/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/typescript": { "version": "4.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", @@ -2040,9 +2001,9 @@ } }, "node_modules/undici": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.12.0.tgz", - "integrity": "sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", + "integrity": "sha512-UDZKtwb2k7KRsK4SdXWG7ErXiL7yTGgLWvk2AXO1JMjgjh404nFo6tWSCM2xMpJwMPx3J8i/vfqEh1zOqvj82Q==", "dependencies": { "busboy": "^1.6.0" }, diff --git a/package.json b/package.json index e8558f3..a22723b 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-beta.3", + "version": "1.0.0", "description": "A Discord bot that checks channel names for blacklisted words and reverts the changes if necessary", "main": "build/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc && shx cp package-lock.json build/package-lock.json", + "start:rebuild": "npm run build && node --enable-source-maps .", "deploy-commands:dev": "ts-node ci/devDeploy.ts", "deploy-commands:prod": "ts-node ci/deploy.ts", "deploy:dev": "npm run build && npm run deploy-commands:dev && docker compose build && docker compose up -d", @@ -40,7 +41,7 @@ "discord.js": "^14.6.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "fs-extra": "^10.1.0", + "fs-extra": "^11.0.0", "moment": "^2.29.4", "pg": "^8.8.0", "typeorm": "^0.3.10" diff --git a/src/cli/guild.ts b/src/cli/guild.ts index 9a95e85..528686a 100644 --- a/src/cli/guild.ts +++ b/src/cli/guild.ts @@ -1,5 +1,4 @@ -import { getGuildSetting, isPremiumActive } from "../tools/data"; -import moment from "moment"; +import { getGuildSetting } from "../tools/data"; import { Badword, database, GuildSetting } from "../data"; import { Console } from "console"; @@ -13,7 +12,6 @@ export default async function execute(args: string[]) { case "info": { if (!args[1]) return printHelp(); const settings = await getGuildSetting(args[1]); - const isPremium = isPremiumActive(settings.isPremiumUntil); const wordCount = await database.getRepository(Badword).count({ where: { guildID: args[1] @@ -21,36 +19,12 @@ export default async function execute(args: string[]) { }); console.log(`Guild ${args[1]}: - - Premium: ${isPremium ? `ACTIVE for ${moment(settings.isPremiumUntil).fromNow(true)}` : "INACTIVE"} - Preserve Settings: ${settings.preserveDataOnGuildLeave ? "ENABLED" : "DISABLED"} - Logchannel: ${settings.notificationChannelID ? `ENABLED (${settings.notificationChannelID})` : "DISABLED"} - blocked Words: ${wordCount}`); break; } - case "setpremium": { - if (!args[1] || !args[2]) return printHelp(); - const settings = await getGuildSetting(args[1]); - - if (args[2].toLowerCase() === "null") { - settings.isPremiumUntil = null; - await database.getRepository(GuildSetting).save(settings); - console.log("Premium status removed for guild " + args[1]); - break; - } - - const date = new Date(args[2]); - if (isNaN(Number(date))) return printHelp(); - - const now = new Date(); - if (now > date) return console.log("Date lies in the past"); - - settings.isPremiumUntil = date; - await database.getRepository(GuildSetting).save(settings); - console.log(`Premium status for guild ${args[1]} is now active for ${moment(date).fromNow(true)}`); - break; - } - case "words": { if (!args[1] || !args[2]) return printWordHelp(); @@ -152,7 +126,6 @@ function printHelp() { console.log(`Usage "guild": guild info [GUILDID] -guild setPremium [GUILDID] [YYYY-MM-DD or NULL] guild words [get|add|remove|clear] guild delete [GUILDID]`); } diff --git a/src/commands/blocklist.ts b/src/commands/blocklist.ts index 6d6878f..d415064 100644 --- a/src/commands/blocklist.ts +++ b/src/commands/blocklist.ts @@ -1,7 +1,7 @@ import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; import { database, Badword } from "../data"; import { IsNull } from "typeorm"; -import { getGuildSetting, isPremiumActive } from "../tools/data"; +import { getGuildSetting } from "../tools/data"; import getDefaultEmbed, { getFailedEmbed, getSuccessEmbed } from "../tools/defaultEmbeds"; import { getGuildChannel } from "../tools/discord"; import { Color, Emoji } from "../tools/design"; @@ -46,7 +46,6 @@ 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; @@ -63,14 +62,14 @@ async function execute(interaction: ChatInputCommandInteraction): Promise } }); - const limit = isPremium ? 100 : 10; + const limit = 100; 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(); + }).catch(() => {}); return; } @@ -89,7 +88,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); return; } @@ -117,7 +116,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise if (logChannel && logChannel.isTextBased()) { logChannel.send({ embeds: [logMessage] - }).catch(); + }).catch(() => {}); } break; } @@ -138,7 +137,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); return; } @@ -151,7 +150,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); const logMessage = getDefaultEmbed(); logMessage.setTitle(`${Emoji.SETTINGS} Word removed`); @@ -164,7 +163,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise if (logChannel && logChannel.isTextBased()) { logChannel.send({ embeds: [logMessage] - }).catch(); + }).catch(() => {}); } break; } diff --git a/src/commands/index.ts b/src/commands/index.ts index 78761e7..86fa211 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -44,6 +44,6 @@ client.on(Events.InteractionCreate, async (interaction) => { await interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); } }); \ No newline at end of file diff --git a/src/commands/info.ts b/src/commands/info.ts index 5d39176..eddc1d0 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -1,7 +1,6 @@ import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; import { IsNull } from "typeorm"; import { Badword, database } from "../data"; -import { getGuildSetting, isPremiumActive } from "../tools/data"; import getDefaultEmbed from "../tools/defaultEmbeds"; import pack from "../../package.json"; import { Color, Emoji } from "../tools/design"; @@ -14,8 +13,6 @@ builder.setDMPermission(false); async function execute(interaction: ChatInputCommandInteraction): Promise { if (!interaction.inGuild()) throw new Error("Command was executed outside guild context"); - const settings = await getGuildSetting(interaction.guildId); - const isPremium = isPremiumActive(settings.isPremiumUntil); const globalBlockedWordsCount = await database.getRepository(Badword).count({ where: { guildID: IsNull() @@ -30,17 +27,13 @@ async function execute(interaction: ChatInputCommandInteraction): Promise const embed = getDefaultEmbed(); embed.setTitle(`${Emoji.SECURITY_CHALLENGE} Channel filter V${pack.version} by AstroGD®`); embed.setDescription(`Codename eu.astrogd.white-leopard`); - embed.setColor(isPremium ? Color.PREMIUM_ORANGE : Color.INFORMING_BLUE); + embed.setColor(Color.INFORMING_BLUE); embed.addFields({ name: "What does this bot do?", value: "This bot checks for blocked words contained in channel names and reverts the changes if found." },{ name: "Author", value: `This bot was created by AstroGD#0001 ${Emoji.ASTROGD} mainly for the official r/Overwatch2 Subreddit Discord server` - },{ - name: "Server status", - value: `${isPremium ? Emoji.PREMIUM : Emoji.SWITCH_OFF} Premium features are ${isPremium ? "enabled" : "disabled"} on this server`, - inline: true },{ name: "Global word count", value: globalBlockedWordsCount.toString(), @@ -57,7 +50,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); } export { diff --git a/src/commands/logchannel.ts b/src/commands/logchannel.ts index d9ac245..0f0f88e 100644 --- a/src/commands/logchannel.ts +++ b/src/commands/logchannel.ts @@ -1,8 +1,8 @@ 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 getDefaultEmbed, { getFailedEmbed, getSuccessEmbed } from "../tools/defaultEmbeds"; import { database, GuildSetting } from "../data"; import { getGuildSetting } from "../tools/data"; -import { getGuildChannel } from "../tools/discord"; +import { getGuildChannel, getChannelPermission } from "../tools/discord"; import { Emoji } from "../tools/design"; const builder = new SlashCommandBuilder(); @@ -20,7 +20,7 @@ builder.addChannelOption((option) => { async function resetNotificationChannel(guildSetting: GuildSetting, interaction: ChatInputCommandInteraction): Promise { const logChannel = guildSetting.notificationChannelID ? await getGuildChannel(guildSetting.id, guildSetting.notificationChannelID) : null; - + guildSetting.notificationChannelID = null; await database.getRepository(GuildSetting).save(guildSetting); @@ -31,11 +31,11 @@ async function resetNotificationChannel(guildSetting: GuildSetting, interaction: name: "This action was performed by", value: `${interaction.user.tag} (${interaction.user.id})` }); - + if (logChannel && logChannel.isTextBased()) { logChannel.send({ embeds: [logEmbed] - }).catch(); + }).catch(() => {}); } const embed = getSuccessEmbed(); @@ -43,7 +43,7 @@ async function resetNotificationChannel(guildSetting: GuildSetting, interaction: interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); } const execute = async (interaction: ChatInputCommandInteraction) => { @@ -55,6 +55,20 @@ const execute = async (interaction: ChatInputCommandInteraction) => { if (!optionVal) return await resetNotificationChannel(guildSetting, interaction); const channel = getTextBasedChannel(optionVal); + if (channel.isDMBased()) return; + + const permissions = await getChannelPermission(channel); + if (!permissions || !permissions.has(PermissionFlagsBits.ViewChannel) || !permissions.has(PermissionFlagsBits.SendMessages)) { + const embed = getFailedEmbed(); + embed.setDescription(`Bot doesn't have permission to view and/or write in channel <#${channel.id}>`); + interaction.reply({ + embeds: [ embed ], + ephemeral: true + }).catch(() => {}); + return; + } + + if (guildSetting.notificationChannelID) { const oldLogChannel = await getGuildChannel(guildSetting.id, guildSetting.notificationChannelID); const embed = getDefaultEmbed(); @@ -68,7 +82,7 @@ const execute = async (interaction: ChatInputCommandInteraction) => { if (oldLogChannel && oldLogChannel.isTextBased()) { oldLogChannel.send({ embeds: [embed] - }).catch(); + }).catch(() => {}); } } @@ -84,16 +98,16 @@ const execute = async (interaction: ChatInputCommandInteraction) => { }); channel.send({ - embeds: [ embed ] - }).catch(); + embeds: [embed] + }).catch(() => {}); const reply = getSuccessEmbed(); reply.setDescription(`Log channel was set to <#${channel.id}>`); interaction.reply({ - embeds: [ reply ], + embeds: [reply], ephemeral: true - }).catch(); + }).catch(() => {}); return; } diff --git a/src/commands/preserveSettings.ts b/src/commands/preserveSettings.ts index 90f0bfd..5536474 100644 --- a/src/commands/preserveSettings.ts +++ b/src/commands/preserveSettings.ts @@ -42,7 +42,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise interaction.reply({ embeds: [embed], ephemeral: true - }).catch(); + }).catch(() => {}); if (!settings.notificationChannelID) return; const logChannel = await getGuildChannel(interaction.guildId, settings.notificationChannelID); @@ -57,7 +57,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise logChannel.send({ embeds: [logEmbed] - }).catch(); + }).catch(() => {}); } export { diff --git a/src/commands/showSettings.ts b/src/commands/showSettings.ts index 2455fb3..6e0f4f2 100644 --- a/src/commands/showSettings.ts +++ b/src/commands/showSettings.ts @@ -1,10 +1,10 @@ -import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { ChatInputCommandInteraction, Guild, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import client from "../client"; import { Badword, database } from "../data"; -import { getGuildSetting, isPremiumActive } from "../tools/data"; +import { getGuildSetting } from "../tools/data"; import getDefaultEmbed from "../tools/defaultEmbeds"; -import moment from "moment"; import { Color, Emoji } from "../tools/design"; -import { getGuildChannel } from "../tools/discord"; +import { getGuildChannel, getChannelPermission } from "../tools/discord"; const builder = new SlashCommandBuilder(); builder.setName("showsettings"); @@ -15,8 +15,13 @@ builder.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild); async function execute(interaction: ChatInputCommandInteraction): Promise { if (!interaction.inGuild()) throw new Error("Interaction was performed outside guild context"); + const guild = await new Promise((resolve) => { + client.guilds.fetch(interaction.guildId).catch(() => {resolve(null)}).then((guild) => {resolve(guild || null)}); + }); + + if (!guild) throw new Error("Guild is unavailable"); + const settings = await getGuildSetting(interaction.guildId); - const isPremium = await isPremiumActive(settings.isPremiumUntil); const wordCount = await database.getRepository(Badword).count({ where: { guildID: interaction.guildId @@ -27,19 +32,15 @@ async function execute(interaction: ChatInputCommandInteraction): Promise const embed = getDefaultEmbed(); embed.setTitle(`Settings from guild ${interaction.guild?.name || ""} (${interaction.guildId})`); - embed.setDescription(isPremium ? `${Emoji.PREMIUM} your subscription ends in ${moment(settings.isPremiumUntil).fromNow(true)}` : `Consider Premium status to get an increased blocklist`); - embed.setColor(isPremium ? Color.PREMIUM_ORANGE : Color.INFORMING_BLUE); + embed.setDescription("Thanks for using this bot"); + embed.setColor(Color.INFORMING_BLUE); embed.addFields({ - name: "Premium", - value: isPremium ? `${Emoji.PREMIUM} active` : `${Emoji.SWITCH_OFF} inactive`, - inline: true - }, { name: "Logchannel", - value: logChannel && logChannel.isTextBased() ? `<#${logChannel.id}>` : "Not configured", + value: settings.notificationChannelID ? `<#${settings.notificationChannelID}>` : "Not configured", inline: true }, { name: "Words in Blocklist", - value: `${wordCount}/${isPremium ? "100" : "10"}`, + value: `${wordCount}/100`, inline: true }, { name: `Preserve data on server leave is ${settings.preserveDataOnGuildLeave ? "active" : "inactive"}`, @@ -49,10 +50,58 @@ async function execute(interaction: ChatInputCommandInteraction): Promise value: "Join the support server at https://go.astrogd.eu/discord" }); + const embeds = []; + embeds.push(embed); + + const invalidLogChannelEmbed = getDefaultEmbed(); + invalidLogChannelEmbed.setColor(Color.WARNING_YELLOW); + invalidLogChannelEmbed.setTitle(`${Emoji.CHAT_DENY} Logchannel is misconfigured!`); + invalidLogChannelEmbed.setDescription("The bot is unable to read and/or write in the configured logChannel. Please adjust your permissions."); + + const missingPermissionEmbed = getDefaultEmbed(); + missingPermissionEmbed.setColor(Color.WARNING_YELLOW); + missingPermissionEmbed.setTitle(`${Emoji.CHAT_DENY} Bot is missing permissions to function properly!`); + missingPermissionEmbed.setDescription("Without these missing Permissions, the bot won't work properly and might lead to unexpected behavior"); + + if (logChannel && logChannel.isTextBased()) { + const permissions = await getChannelPermission(logChannel); + if (!permissions || !permissions.has(PermissionFlagsBits.ViewChannel) || !permissions.has(PermissionFlagsBits.SendMessages)) { + embeds.push(invalidLogChannelEmbed); + } + } + + if ((!logChannel || !logChannel.isTextBased()) && settings.notificationChannelID) { + embeds.push(invalidLogChannelEmbed); + } + + const member = await guild.members.fetchMe(); + if (!member.permissions.has(PermissionFlagsBits.ViewAuditLog)) { + missingPermissionEmbed.addFields({ + name: "View Audit Logs", + value: "With this permission the bot can determine who created or updated a channel with a blocked word" + }); + } + + if (!member.permissions.has(PermissionFlagsBits.ViewChannel)) { + missingPermissionEmbed.addFields({ + name: "View Channels", + value: "If the bot can't see a channel, it won't be able to detect blocked words in it" + }); + } + + if (!member.permissions.has(PermissionFlagsBits.ManageChannels)) { + missingPermissionEmbed.addFields({ + name: "Manage Channels", + value: "If the bot can't delete a channel, it can't protect your server from channels with blocked words" + }); + } + + if (missingPermissionEmbed.data.fields?.length || 0 > 0) embeds.push(missingPermissionEmbed); + interaction.reply({ - embeds: [embed], + embeds: embeds, ephemeral: true - }).catch(); + }).catch(() => {}); } export { diff --git a/src/commands/showblocklist.ts b/src/commands/showblocklist.ts index 347f9a4..7a6ce0b 100644 --- a/src/commands/showblocklist.ts +++ b/src/commands/showblocklist.ts @@ -1,7 +1,6 @@ import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; import { IsNull } from "typeorm"; import { Badword, database } from "../data"; -import { getGuildSetting, isPremiumActive } from "../tools/data"; const builder = new SlashCommandBuilder(); builder.setName("showblocklist"); @@ -11,9 +10,6 @@ builder.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels); async function execute(interaction: ChatInputCommandInteraction): Promise { if (!interaction.inGuild()) throw new Error("Command was executed outside guild context"); - - const settings = await getGuildSetting(interaction.guildId); - const isPremium = isPremiumActive(settings.isPremiumUntil); const guildBadWords = await database.getRepository(Badword).find({ select: { @@ -30,9 +26,9 @@ async function execute(interaction: ChatInputCommandInteraction): Promise }); 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)} ||`, + 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}/100)\`\`\`\n||${guildBadWords.map((word) => word.value).reduce((prev, next) => prev + ", " + next, "").slice(2)} ||`, ephemeral: true - }).catch(); + }).catch(() => {}); } export { diff --git a/src/data/migrations/1669686263307-data.ts b/src/data/migrations/1669686263307-data.ts new file mode 100644 index 0000000..266970a --- /dev/null +++ b/src/data/migrations/1669686263307-data.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class data1669686263307 implements MigrationInterface { + name = 'data1669686263307' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "guild_setting" DROP COLUMN "isPremiumUntil" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "guild_setting" + ADD "isPremiumUntil" TIMESTAMP + `); + } + +} diff --git a/src/data/model/guildSetting.ts b/src/data/model/guildSetting.ts index 65c032c..4f1e1f6 100644 --- a/src/data/model/guildSetting.ts +++ b/src/data/model/guildSetting.ts @@ -8,9 +8,6 @@ export class GuildSetting { @Column("varchar", { nullable: true, default: null }) notificationChannelID!: string | null; - @Column("timestamp", { nullable: true, default: null }) - isPremiumUntil!: Date | null; - @Column("boolean", { default: false }) preserveDataOnGuildLeave!: boolean } \ No newline at end of file diff --git a/src/events/channelCreate.ts b/src/events/channelCreate.ts new file mode 100644 index 0000000..0549c36 --- /dev/null +++ b/src/events/channelCreate.ts @@ -0,0 +1,109 @@ +import client from "../client"; +import { AuditLogEvent, Events, GuildAuditLogsEntry, PermissionFlagsBits, User } from "discord.js"; +import { getGuildSetting } from "../tools/data"; +import { Badword, database } from "../data"; +import { IsNull } from "typeorm"; +import getDefaultEmbed, { getFailedEmbed } from "../tools/defaultEmbeds"; +import { getGuildChannel } from "../tools/discord"; +import { Color, Emoji } from "../tools/design"; + +client.on(Events.ChannelCreate, async (newChannel) => { + if (newChannel.isDMBased()) return; + const name = newChannel.name.toLowerCase(); + + const guild = newChannel.guild; + const settings = await getGuildSetting(guild.id); + + const globalBlocklist = await database.getRepository(Badword).find({ + where: { + guildID: IsNull() + } + }); + const localBlocklist = await database.getRepository(Badword).find({ + where: { + guildID: guild.id + } + }); + + const blocklist = [...globalBlocklist, ...localBlocklist]; + let found: string | null = null; + + for (let i = 0; i < blocklist.length; i++) { + const word = blocklist[i]; + if (!word) continue; + + if (!name.includes(word.value)) continue; + found = word.value; + break; + } + + if (found === null) return; + + let responsibleUser: User | null; + + try { + const clientMember = await guild.members.fetchMe(); + const canSeeAuditLog = clientMember.permissions.has(PermissionFlagsBits.ViewAuditLog); + const auditLogs = canSeeAuditLog ? await guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelCreate, + limit: 50 + }) : undefined; + + const change = auditLogs?.entries.filter((entry) => { + if (entry.target.id !== newChannel.id) return false; + + return entry.changes.filter((change) => { + return change.key === "name" && change.new === newChannel.name; + }).length > 0; + }).reduce>((unknown, curr) => { + if (!unknown) return curr; + const prev = unknown as GuildAuditLogsEntry + return curr.createdTimestamp > prev.createdTimestamp ? curr : prev; + }, null); + + responsibleUser = change?.executor || null; + } catch (error) { + responsibleUser = null; + } + + const logChannel = settings.notificationChannelID ? await getGuildChannel(guild.id, settings.notificationChannelID) : null; + + try { + if (!newChannel.deletable) throw new Error("Missing permissions to delete channel"); + await newChannel.delete("[Automated] Detected blocked word in channel name"); + } catch (error) { + if (!logChannel || !logChannel.isTextBased()) return; + const embed = getFailedEmbed(); + embed.setDescription(`Couldn't delete <#${newChannel.id}> (${newChannel.id}): ${error instanceof Error ? error.message : error}`); + embed.addFields({ + name: "Detected banned word:", + value: `||${found}||` + }, { + name: "Channel created by:", + value: responsibleUser ? `${responsibleUser.tag} (${responsibleUser.id})` : "`Couldn't detect responsible user :(`" + }); + + logChannel.send({ + embeds: [embed] + }).catch(() => {}); + return; + } + + if (!logChannel || !logChannel.isTextBased()) return; + const embed = getDefaultEmbed(); + embed.setTitle(`${Emoji.SECURITY_CHALLENGE_FAILED} Blocked word detected`); + embed.setDescription(`||#${newChannel.name}|| (${newChannel.id}) has been deleted because its name contained a blocked word.`); + embed.setColor(Color.WARNING_YELLOW); + embed.addFields({ + name: "Detected banned word:", + value: `||${found}||`, + inline: true + }, { + name: "Channel created by:", + value: responsibleUser ? `${responsibleUser.tag} (${responsibleUser.id})` : "`Couldn't detect responsible user :(`" + }); + + logChannel.send({ + embeds: [embed] + }).catch(() => {}); +}); \ No newline at end of file diff --git a/src/events/channelUpdate.ts b/src/events/channelUpdate.ts index 9d41d2b..9e297b2 100644 --- a/src/events/channelUpdate.ts +++ b/src/events/channelUpdate.ts @@ -1,6 +1,6 @@ import client from "../client"; -import { Events } from "discord.js"; -import { getGuildSetting, isPremiumActive } from "../tools/data"; +import { AuditLogEvent, Events, GuildAuditLogsEntry, PermissionFlagsBits, User } from "discord.js"; +import { getGuildSetting } from "../tools/data"; import { Badword, database } from "../data"; import { IsNull } from "typeorm"; import getDefaultEmbed, { getFailedEmbed } from "../tools/defaultEmbeds"; @@ -10,11 +10,9 @@ import { Color, Emoji } from "../tools/design"; client.on(Events.ChannelUpdate, async (oldChannel, newChannel) => { if (oldChannel.isDMBased() || newChannel.isDMBased()) return; const name = newChannel.name.toLowerCase(); - if (name === "censored") return; const guild = oldChannel.guild; const settings = await getGuildSetting(guild.id); - const isPremium = isPremiumActive(settings.isPremiumUntil); const globalBlocklist = await database.getRepository(Badword).find({ where: { @@ -29,7 +27,7 @@ client.on(Events.ChannelUpdate, async (oldChannel, newChannel) => { const blocklist = [...globalBlocklist, ...localBlocklist]; let found: string | null = null; - + for (let i = 0; i < blocklist.length; i++) { const word = blocklist[i]; if (!word) continue; @@ -41,45 +39,71 @@ client.on(Events.ChannelUpdate, async (oldChannel, newChannel) => { if (found === null) return; + let responsibleUser: User | null; + + try { + const clientMember = await guild.members.fetchMe(); + const canSeeAuditLog = clientMember.permissions.has(PermissionFlagsBits.ViewAuditLog); + const auditLogs = canSeeAuditLog ? await guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelUpdate, + limit: 50 + }) : undefined; + + const change = auditLogs?.entries.filter((entry) => { + if (entry.target.id !== newChannel.id) return false; + + return entry.changes.filter((change) => { + return change.key === "name" && change.new === newChannel.name; + }).length > 0; + }).reduce>((unknown, curr) => { + if (!unknown) return curr; + const prev = unknown as GuildAuditLogsEntry + return curr.createdTimestamp > prev.createdTimestamp ? curr : prev; + }, null); + + responsibleUser = change?.executor || null; + } catch (error) { + responsibleUser = null; + } + const logChannel = settings.notificationChannelID ? await getGuildChannel(guild.id, settings.notificationChannelID) : null; try { - await newChannel.setName("CENSORED", `[Automated] Detected blocked word in channel name. Name has been censored`); + if (!newChannel.deletable) throw new Error("Missing permissions to delete channel"); + await newChannel.delete("[Automated] Detected blocked word in channel name"); } catch (error) { if (!logChannel || !logChannel.isTextBased()) return; const embed = getFailedEmbed(); - embed.setDescription(`Couldn't censor <#${newChannel.id}> (${newChannel.id}): ${error instanceof Error ? error.message : error}`); - if (isPremium) embed.addFields({ + embed.setDescription(`Couldn't delete <#${newChannel.id}> (${newChannel.id}): ${error instanceof Error ? error.message : error}`); + embed.addFields({ name: "Detected banned word:", value: `||${found}||` - },{ - name: "Old channel name:", - value: `||${name}||`, - inline: true + }, { + name: "Channel renamed by:", + value: responsibleUser ? `${responsibleUser.tag} (${responsibleUser.id})` : "`Couldn't detect responsible user :(`" }); logChannel.send({ embeds: [embed] - }).catch(); + }).catch(() => {}); return; } if (!logChannel || !logChannel.isTextBased()) return; const embed = getDefaultEmbed(); embed.setTitle(`${Emoji.SECURITY_CHALLENGE_FAILED} Blocked word detected`); - embed.setDescription(`<#${newChannel.id}> (${newChannel.id}) has been renamed because its name contained a blocked word.`); + embed.setDescription(`||#${newChannel.name}|| (${newChannel.id}) has been deleted because its name contained a blocked word.`); embed.setColor(Color.WARNING_YELLOW); - if (isPremium) embed.addFields({ + embed.addFields({ name: "Detected banned word:", value: `||${found}||`, inline: true - },{ - name: "Old channel name:", - value: `||${name}||`, - inline: true + }, { + name: "Channel renamed by:", + value: responsibleUser ? `${responsibleUser.tag} (${responsibleUser.id})` : "`Couldn't detect responsible user :(`" }); logChannel.send({ embeds: [embed] - }).catch(); + }).catch(() => {}); }); \ No newline at end of file diff --git a/src/events/index.ts b/src/events/index.ts index ab87147..ca7292e 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,2 +1,3 @@ import "./channelUpdate"; -import "./guildDelete"; \ No newline at end of file +import "./guildDelete"; +import "./channelCreate"; \ No newline at end of file diff --git a/src/tools/data.ts b/src/tools/data.ts index 0addaf6..24d8526 100644 --- a/src/tools/data.ts +++ b/src/tools/data.ts @@ -10,18 +10,9 @@ export async function getGuildSetting(guildID: string): Promise { if (!guildSetting) { guildSetting = new GuildSetting(); guildSetting.id = guildID; - guildSetting.isPremiumUntil = null; guildSetting.notificationChannelID = null; guildSetting.preserveDataOnGuildLeave = false; } return guildSetting; -} - -export function isPremiumActive(timestamp: Date | null): boolean { - 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/discord.ts b/src/tools/discord.ts index baa2d0f..1717f26 100644 --- a/src/tools/discord.ts +++ b/src/tools/discord.ts @@ -1,10 +1,25 @@ -import { GuildBasedChannel } from "discord.js"; +import { GuildBasedChannel, GuildTextBasedChannel, PermissionsBitField } 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; + try { + const guild = await client.guilds.fetch(guildID); + if (!guild) return null; - const channel = await guild.channels.fetch(channelID); - return channel; + const channel = await guild.channels.fetch(channelID); + return channel; + } catch (_error) { + return null; + } +} + +export async function getChannelPermission(channel: GuildTextBasedChannel): Promise | null> { + try { + const guildMember = await channel.guild.members.fetchMe(); + if (!guildMember) return null; + + return channel.permissionsFor(guildMember); + } catch (error) { + return null; + } } \ No newline at end of file