From 261627570d037a997974509c8ef6d8cab9d40ab1 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Wed, 23 Nov 2022 13:23:25 +0100 Subject: [PATCH 01/10] Base structure --- .gitignore | 3 + LICENSE.md | 132 +++++++++++ index.ts | 13 ++ package-lock.json | 435 +++++++++++++++++++++++++++++++++++ package.json | 29 +++ src/client/index.ts | 18 ++ src/commands/index.ts | 0 src/commands/notification.ts | 51 ++++ src/tools/consoleSwapper.ts | 62 +++++ src/tools/defaultEmbeds.ts | 24 ++ tsconfig.json | 103 +++++++++ 11 files changed, 870 insertions(+) create mode 100644 LICENSE.md create mode 100644 index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/client/index.ts create mode 100644 src/commands/index.ts create mode 100644 src/commands/notification.ts create mode 100644 src/tools/consoleSwapper.ts create mode 100644 src/tools/defaultEmbeds.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 6704566..5d58840 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Build files +build + # Logs logs *.log diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a3b5dfb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,132 @@ +You can find the original version of this license here: https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode + +A short and easily understandable version of the license can be found here: https://creativecommons.org/licenses/by-nc-nd/4.0/ + + +# Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +## Section 1 – Definitions. + +**a) Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + +**b) Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +**c) Effective Technological Measures** means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + +**d) Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + +**e) Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + +**f) Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + +**g) Licensor** means the individual(s) or entity(ies) granting rights under this Public License. + +**h) NonCommercial** means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. + +**i) Share** means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + +**j) Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + +**k) You** means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +## Section 2 – Scope. + +**a) License grant.** + +1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + **A:** reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and + + **B:** produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only. + +2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. +3. __Term.__ The term of this Public License is specified in Section 6(a). +4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. +5. __Downstream recipients.__ + + **A:** __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + **B:** __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. +6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +**b) Other rights.** +1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. +2. Patent and trademark rights are not licensed under this Public License. +3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. + +## Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + +**a) Attribution.** + +1. If You Share the Licensed Material, You must: + + **A:** retain the following if it is supplied by the Licensor with the Licensed Material: + - identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + - a copyright notice; + - a notice that refers to this Public License; + - a notice that refers to the disclaimer of warranties; + - a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + **B:** indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + **C:** indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + +For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. + +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. +3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + +## Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + +**A:** for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material; + +**B:** if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + +**C:** You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +## Section 5 – Disclaimer of Warranties and Limitation of Liability. + +**A:** Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + +**B:** To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + +**C:** The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +## Section 6 – Term and Termination. + +**A:** This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + +**B:** Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or +2. upon express reinstatement by the Licensor. + +For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + +**C:** For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + +**D:** Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +## Section 7 – Other Terms and Conditions. + +**A:** The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + +**B:** Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +## Section 8 – Interpretation. + +**A:** For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + +**B:** To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + +**C:** No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + +**D:** Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..853515e --- /dev/null +++ b/index.ts @@ -0,0 +1,13 @@ +import { config } from "dotenv"; +import swapConsole from "./src/tools/consoleSwapper"; +import "./src/client"; +config(); +swapConsole(); + +function shutdown() { + console.log("Shutdown request received"); + process.exit(); +} + +process.on("SIGINT", shutdown); +process.on("SIGHUP", shutdown); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d056c55 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,435 @@ +{ + "name": "eu.astrogd.white-leopard", + "version": "1.0.0-alpha.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eu.astrogd.white-leopard", + "version": "1.0.0-alpha.1", + "license": "CC-BY-NC-ND-4.0", + "dependencies": { + "discord.js": "^14.6.0", + "dotenv": "^16.0.3", + "fs-extra": "^10.1.0" + }, + "devDependencies": { + "@types/fs-extra": "^9.0.13", + "@types/node": "^18.11.9", + "typescript": "^4.9.3" + } + }, + "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==", + "dependencies": { + "@discordjs/util": "^0.1.0", + "@sapphire/shapeshift": "^3.7.0", + "discord-api-types": "^0.37.12", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.1", + "tslib": "^2.4.0" + }, + "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==", + "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==", + "dependencies": { + "@discordjs/collection": "^1.2.0", + "@discordjs/util": "^0.1.0", + "@sapphire/async-queue": "^1.5.0", + "@sapphire/snowflake": "^3.2.2", + "discord-api-types": "^0.37.12", + "file-type": "^18.0.0", + "tslib": "^2.4.0", + "undici": "^5.11.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/util": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz", + "integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", + "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "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==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash.uniqwith": "^4.5.0" + }, + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.2.tgz", + "integrity": "sha512-ula2O0kpSZtX9rKXNeQMrHwNd7E4jPDJYUXmEGTFdMRfyfMw+FPyh04oKMjAiDuOi64bYgVkOV3MjK+loImFhQ==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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==" + }, + "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==", + "dependencies": { + "@discordjs/builders": "^1.3.0", + "@discordjs/collection": "^1.2.0", + "@discordjs/rest": "^1.3.0", + "@discordjs/util": "^0.1.0", + "@sapphire/snowflake": "^3.2.2", + "@types/ws": "^8.5.3", + "discord-api-types": "^0.37.12", + "fast-deep-equal": "^3.1.3", + "lodash.snakecase": "^4.1.1", + "tslib": "^2.4.0", + "undici": "^5.11.0", + "ws": "^8.9.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/file-type": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.0.0.tgz", + "integrity": "sha512-jjMwFpnW8PKofLE/4ohlhqwDk5k0NC6iy0UHAJFKoY1fQeGMN0GDdLgHQrvCbSpMwbqzoCZhRI5dETCZna5qVA==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "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==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/lodash.uniqwith": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", + "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==" + }, + "node_modules/peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", + "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==" + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, + "node_modules/typescript": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.12.0.tgz", + "integrity": "sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.18" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f9d437e --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "eu.astrogd.white-leopard", + "version": "1.0.0-alpha.1", + "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" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/r-Overwatch2/eu.astrogd.white-leopard.git" + }, + "author": "AstroGD Lukas Weber (https://www.astrogd.eu)", + "license": "CC-BY-NC-ND-4.0", + "bugs": { + "url": "https://github.com/r-Overwatch2/eu.astrogd.white-leopard/issues" + }, + "homepage": "https://github.com/r-Overwatch2/eu.astrogd.white-leopard#readme", + "devDependencies": { + "@types/fs-extra": "^9.0.13", + "@types/node": "^18.11.9", + "typescript": "^4.9.3" + }, + "dependencies": { + "discord.js": "^14.6.0", + "dotenv": "^16.0.3", + "fs-extra": "^10.1.0" + } +} diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 0000000..68199fb --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1,18 @@ +import { Client, GatewayIntentBits } from "discord.js"; + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds + ] +}); + +const token = process.env["TOKEN"]; +if (!token) throw new ReferenceError("TOKEN environment variable is missing"); + +client.login(token); + +client.on("ready", () => { + console.log(`Connected to Discord API. Bot account is ${client.user?.tag} (${client.user?.id})`); +}); + +export default client; \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/commands/notification.ts b/src/commands/notification.ts new file mode 100644 index 0000000..76d1884 --- /dev/null +++ b/src/commands/notification.ts @@ -0,0 +1,51 @@ +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ChatInputCommandInteraction, NewsChannel, TextBasedChannel, CategoryChannel, StageChannel, TextChannel, PrivateThreadChannel, PublicThreadChannel, VoiceChannel, APIInteractionDataResolvedChannel, ForumChannel } from "discord.js"; +import { getSuccessEmbed } from "../tools/defaultEmbeds"; + +const builder = new SlashCommandBuilder(); +builder.setName("logchannel"); +builder.setDescription("Configures the log channel"); +builder.setDMPermission(false); +builder.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels); +builder.addChannelOption((option) => { + option.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement); + option.setName("Channel"); + option.setDescription("The channel to send notifications to"); + option.setRequired(true); + return option; +}); + +const execute = (interaction: ChatInputCommandInteraction) => { + const channel:TextBasedChannel = getTextBasedChannel(interaction.options.getChannel("Channel", true)); + + //TODO Save channel in config + + const embed = getSuccessEmbed(); + embed.setDescription(`Log channel has been set to <#${channel.id}>`); + interaction.reply({ + embeds: [embed], + ephemeral: true + }); + + const logMessage = getSuccessEmbed(); + logMessage.setDescription("This channel has been selected as the log channel"); + logMessage.addFields({ + name: "This action was performed by", + value: `${interaction.user.tag} (${interaction.user.id})` + }); + channel.send({ + embeds: [logMessage] + }); +} + +function getTextBasedChannel(channel: CategoryChannel | NewsChannel | StageChannel | TextChannel | PrivateThreadChannel | PublicThreadChannel | VoiceChannel | ForumChannel | APIInteractionDataResolvedChannel): TextBasedChannel { + if (channel.type === ChannelType.DM || channel.type === ChannelType.GroupDM || channel.type === ChannelType.GuildAnnouncement || channel.type === ChannelType.GuildText || channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread || channel.type === ChannelType.GuildVoice) { + return channel as TextBasedChannel; + } + + throw new TypeError("Channel is not a text based channel"); +} + +export { + builder, + execute +} \ No newline at end of file diff --git a/src/tools/consoleSwapper.ts b/src/tools/consoleSwapper.ts new file mode 100644 index 0000000..c9d7a46 --- /dev/null +++ b/src/tools/consoleSwapper.ts @@ -0,0 +1,62 @@ +/* eslint-disable prefer-rest-params */ +/** + * This module swaps the default console outputs to own functions and adds a timestamp to each message + */ + + import path from "path"; + import fs from "fs-extra"; + + export default function() { + fs.ensureDirSync(path.join(__dirname, "../data/log/")); + + const callDate = new Date(); + const logFileName = `${callDate.getUTCFullYear()}-${`0${callDate.getUTCMonth() + 1}`.slice(-2)}-${`0${callDate.getUTCDate()}`.slice(-2)}-${`0${callDate.getUTCHours()}`.slice(-2)}-${`0${callDate.getUTCMinutes()}`.slice(-2)}-${`0${callDate.getUTCSeconds()}`.slice(-2)}-${`00${callDate.getUTCMilliseconds()}`.slice(-3)}.log`; + const logFile = fs.createWriteStream(path.join(__dirname, `../data/log/${logFileName}`)); + const out = new console.Console(process.stdout, process.stderr, true); + const logConsole = new console.Console(logFile); + + function log() { + const now = new Date(); + const Prepend = `[i] [${now.getUTCFullYear()}-${`0${now.getUTCMonth() + 1}`.slice(-2)}-${`0${now.getUTCDate()}`.slice(-2)} ${`0${now.getUTCHours()}`.slice(-2)}:${`0${now.getUTCMinutes()}`.slice(-2)}:${`0${now.getUTCSeconds()}`.slice(-2)}.${`00${now.getUTCMilliseconds()}`.slice(-3)}] `; + arguments[0] = `${Prepend}${arguments[0].toString().normalize()}`; + + for (let i = 0; i < arguments.length; i++) { + arguments[i] = arguments[i].split("\n").join(`\n${new Array(31).join(" ")}`); + } + + out.log(...arguments); + logConsole.log(...arguments); + } + + function warn() { + const now = new Date(); + const Prepend = `[W] [${now.getUTCFullYear()}-${`0${now.getUTCMonth() + 1}`.slice(-2)}-${`0${now.getUTCDate()}`.slice(-2)} ${`0${now.getUTCHours()}`.slice(-2)}:${`0${now.getUTCMinutes()}`.slice(-2)}:${`0${now.getUTCSeconds()}`.slice(-2)}.${`00${now.getUTCMilliseconds()}`.slice(-3)}] `; + arguments[0] = `${Prepend}${arguments[0].toString().normalize()}`; + + for (let i = 0; i < arguments.length; i++) { + const argument = arguments[i].toString().normalize(); + arguments[i] = argument.split("\n").join(`\n${new Array(31).join(" ")}`); + } + + out.warn(...arguments); + logConsole.warn(...arguments); + } + + function error() { + const now = new Date(); + const Prepend = `==== [ERROR] [${now.getUTCFullYear()}-${`0${now.getUTCMonth() + 1}`.slice(-2)}-${`0${now.getUTCDate()}`.slice(-2)} ${`0${now.getUTCHours()}`.slice(-2)}:${`0${now.getUTCMinutes()}`.slice(-2)}:${`0${now.getUTCSeconds()}`.slice(-2)}.${`00${now.getUTCMilliseconds()}`.slice(-3)}] ====\n`; + arguments[0] = `${Prepend}${arguments[0].toString().normalize()}`; + + for (let i = 0; i < arguments.length; i++) { + arguments[i] = arguments[i].toString().normalize(); + } + + out.error(...arguments); + logConsole.error(...arguments); + } + + console.log = log; + console.warn = warn; + console.error = error; + } + \ No newline at end of file diff --git a/src/tools/defaultEmbeds.ts b/src/tools/defaultEmbeds.ts new file mode 100644 index 0000000..4338fe4 --- /dev/null +++ b/src/tools/defaultEmbeds.ts @@ -0,0 +1,24 @@ +import pack from "../../package.json"; +import { EmbedBuilder } from "discord.js"; +import client from "../client"; + +// const _coolColors = [0x054566]; + +export default function getDefaultEmbed(): EmbedBuilder { + const embed = new EmbedBuilder(); + embed.setFooter({ + text: `Channel filter V${pack.version} by AstroGD®`, + iconURL: client.user?.avatarURL() || undefined, + }); + embed.setTimestamp(new Date()); + embed.setColor(0x3682cc); + + return embed; +} + +export function getSuccessEmbed(): EmbedBuilder { + const embed = getDefaultEmbed(); + embed.setTitle("Success"); + embed.setColor(0x32d122); + return embed; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c6c0127 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From d8365128fce25fc0354eaa0f8792a1b31dd63ef1 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 02:12:52 +0100 Subject: [PATCH 02/10] Base structure #2 --- Dockerfile | 13 + README.md | 14 + ci/deploy.ts | 33 + ci/devDeploy.ts | 33 + docker-compose.yml | 25 + index.ts | 4 +- package-lock.json | 1115 ++++++++++++++++++++- package.json | 16 +- src/client/index.ts | 4 +- src/commands/ci.ts | 7 + src/commands/index.ts | 39 + src/commands/notification.ts | 124 ++- src/data/dataSource.ts | 31 + src/data/index.ts | 9 + src/data/migrations/1669251793386-data.ts | 34 + src/data/model/badword.ts | 13 + src/data/model/guildSetting.ts | 13 + src/data/model/index.ts | 7 + src/tools/consoleSwapper.ts | 15 +- tsconfig.json | 20 +- 20 files changed, 1528 insertions(+), 41 deletions(-) create mode 100644 Dockerfile create mode 100644 ci/deploy.ts create mode 100644 ci/devDeploy.ts create mode 100644 docker-compose.yml create mode 100644 src/commands/ci.ts create mode 100644 src/data/dataSource.ts create mode 100644 src/data/index.ts create mode 100644 src/data/migrations/1669251793386-data.ts create mode 100644 src/data/model/badword.ts create mode 100644 src/data/model/guildSetting.ts create mode 100644 src/data/model/index.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..144ce89 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18-alpine AS builder +ENV NODE_ENV=production +COPY build/package*.json ./ +RUN apk add --no-cache python3 make g++ +RUN npm install --omit=dev + +FROM node:18-alpine AS app +WORKDIR /usr/src/app +ENV NODE_ENV=production +COPY --from=builder node_modules ./node_modules +COPY build/ . +VOLUME [ "/usr/src/app/data" ] +CMD ["node", "--enable-source-maps", "index.js"] \ No newline at end of file diff --git a/README.md b/README.md index 17ea1af..ea032b8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # eu.astrogd.white-leopard A Discord bot that checks Discord channel names for banned words and prevents renaming of them + +## Environment variables +| Name | Description | Required | Example | +| :---------- | :------------------------------------------------------------ | :------: | :------------------ | +| TOKEN | Discord bot token to log into the API with | ▶️/🚀 | NzYzMDP3MzE1Mzky... | +| DB_HOST | Hostname of the database | ▶️ | 127.0.0.1:3546 | +| DB_USERNAME | Username of the database | ▶️ | root | +| DB_PASSWORD | Password of the database | ▶️ | abc123 | +| DB_DATABASE | Database name | ▶️ | white-leopard | +| CLIENT_ID | Client ID of the Discord appication associated with the token | 🚀 | 763035392692274 | + +### Icon explanation: +- ▶️ = Required in runtime +- 🚀 = Required during CI/CD \ No newline at end of file diff --git a/ci/deploy.ts b/ci/deploy.ts new file mode 100644 index 0000000..f454013 --- /dev/null +++ b/ci/deploy.ts @@ -0,0 +1,33 @@ +// Dotenv initialization +import path from "path"; +import dotenv from "dotenv"; +dotenv.config({ path: path.join(__dirname, "../.env") }); + +// Environment checking +const TOKEN = process.env["TOKEN"]; +const CLIENT_ID = process.env["CLIENT_ID"]; + +if (!TOKEN) throw new ReferenceError("Environment variable TOKEN is missing"); +if (!CLIENT_ID) throw new ReferenceError("Environment variable CLIENT_ID is missing"); + +// Deployment +import { REST, Routes } from "discord.js"; +import { array as commands } from "../src/commands/ci"; + +const API = new REST({ version: "10" }).setToken(TOKEN); + +(async () => { + try { + console.log("Start deploying slash commands globally"); + + const data = await API.put( + Routes.applicationCommands(CLIENT_ID), + { body: commands }, + ); + + console.log(`Successfully deployed ${(data as any).length} commands`); + } + catch (e) { + console.error(e); + } +})(); \ No newline at end of file diff --git a/ci/devDeploy.ts b/ci/devDeploy.ts new file mode 100644 index 0000000..59fc127 --- /dev/null +++ b/ci/devDeploy.ts @@ -0,0 +1,33 @@ +// Dotenv initialization +import path from "path"; +import dotenv from "dotenv"; +dotenv.config({ path: path.join(__dirname, "../.env") }); + +// Environment checking +const TOKEN = process.env["TOKEN"]; +const CLIENT_ID = process.env["CLIENT_ID"]; + +if (!TOKEN) throw new ReferenceError("Environment variable TOKEN is missing"); +if (!CLIENT_ID) throw new ReferenceError("Environment variable CLIENT_ID is missing"); + +// Deployment +import { REST, Routes } from "discord.js"; +import { array as commands } from "../src/commands/ci"; + +const API = new REST({ version: "10" }).setToken(TOKEN); + +(async () => { + try { + console.log("Start deploying slash commands for Dev guild"); + + const data = await API.put( + Routes.applicationGuildCommands(CLIENT_ID, "853049355984437269"), + { body: commands }, + ); + + console.log(`Successfully deployed ${(data as any).length} commands`); + } + catch (e) { + console.error(e); + } +})(); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..263a65c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: "2" +services: + app: + image: astrogd/white-leopard + build: ./ + depends_on: + - database + restart: always + environment: + - TOKEN=$TOKEN + - DB_HOST=database:5432 + - DB_USERNAME=$DB_USERNAME + - DB_PASSWORD=$DB_PASSWORD + - DB_DATABASE=$DB_DATABASE + database: + image: postgres:latest + restart: always + ports: + - 5432:5432 + environment: + - POSTGRES_USER=$DB_USERNAME + - POSTGRES_PASSWORD=$DB_PASSWORD + - POSTGRES_DB=$DB_DATABASE + expose: + - 5432 \ No newline at end of file diff --git a/index.ts b/index.ts index 853515e..0929e98 100644 --- a/index.ts +++ b/index.ts @@ -1,9 +1,11 @@ import { config } from "dotenv"; import swapConsole from "./src/tools/consoleSwapper"; -import "./src/client"; + config(); swapConsole(); +import "./src/client"; + function shutdown() { console.log("Shutdown request received"); process.exit(); diff --git a/package-lock.json b/package-lock.json index d056c55..c0f745d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,30 @@ "dependencies": { "discord.js": "^14.6.0", "dotenv": "^16.0.3", - "fs-extra": "^10.1.0" + "fs-extra": "^10.1.0", + "pg": "^8.8.0", + "typeorm": "^0.3.10" }, "devDependencies": { "@types/fs-extra": "^9.0.13", "@types/node": "^18.11.9", + "shx": "^0.3.4", + "ts-node": "^10.9.1", "typescript": "^4.9.3" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@discordjs/builders": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.3.0.tgz", @@ -69,6 +85,31 @@ "node": ">=16.9.0" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "devOptional": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "devOptional": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@sapphire/async-queue": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", @@ -100,11 +141,40 @@ "npm": ">=7.0.0" } }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "devOptional": true + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -127,6 +197,137 @@ "@types/node": "*" } }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -138,6 +339,153 @@ "node": ">=10.16.0" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "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/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/discord-api-types": { "version": "0.37.19", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.19.tgz", @@ -173,6 +521,19 @@ "node": ">=12" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -207,11 +568,77 @@ "node": ">=12" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -231,11 +658,60 @@ } ] }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -257,6 +733,111 @@ "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/peek-readable": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", @@ -269,6 +850,115 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/pg": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.5.2", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", + "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -297,6 +987,48 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -316,6 +1048,64 @@ } ] }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -332,6 +1122,30 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strtok3": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", @@ -348,6 +1162,48 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/token-types": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", @@ -369,16 +1225,166 @@ "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==" }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "node_modules/typeorm": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.10.tgz", + "integrity": "sha512-VMKiM84EpJQ+Mz9xDIPqnfplWhyUy1d8ccaKdMY9obifxJOTFnv8GYVyPsGwG8Lk7Nb8MlttHyHWENGAhBA3WA==", + "dependencies": { + "@sqltools/formatter": "^1.2.2", + "app-root-path": "^3.0.0", + "buffer": "^6.0.3", + "chalk": "^4.1.0", + "cli-highlight": "^2.1.11", + "date-fns": "^2.28.0", + "debug": "^4.3.3", + "dotenv": "^16.0.0", + "glob": "^7.2.0", + "js-yaml": "^4.1.0", + "mkdirp": "^1.0.4", + "reflect-metadata": "^0.1.13", + "sha.js": "^2.4.11", + "tslib": "^2.3.1", + "uuid": "^8.3.2", + "xml2js": "^0.4.23", + "yargs": "^17.3.1" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">= 12.9.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^7.1.2", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^3.6.0", + "mssql": "^7.3.0", + "mysql2": "^2.2.5", + "oracledb": "^5.1.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, "node_modules/typescript": { "version": "4.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -411,6 +1417,41 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, "node_modules/ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", @@ -430,6 +1471,76 @@ "optional": true } } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } } } } diff --git a/package.json b/package.json index f9d437e..2b75eb6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,15 @@ "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" + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc && shx cp package-lock.json build/package-lock.json", + "deploy-dev": "ts-node ci/devDeploy.ts", + "start": "npm run build && npm run deploy-dev && docker-compose up --no-start && docker compose start database && node --enable-source-maps .", + "migration:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d src/data/dataSource.ts -p src/data/migrations/data", + "migration:run": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:run -d src/data/dataSource.ts", + "migration:revert": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:revert -d src/data/dataSource.ts", + "migration:show": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:show -d src/data/dataSource.ts", + "migration:check": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate --check -d src/data/dataSource.ts src/data/migrations/data" }, "repository": { "type": "git", @@ -19,11 +27,15 @@ "devDependencies": { "@types/fs-extra": "^9.0.13", "@types/node": "^18.11.9", + "shx": "^0.3.4", + "ts-node": "^10.9.1", "typescript": "^4.9.3" }, "dependencies": { "discord.js": "^14.6.0", "dotenv": "^16.0.3", - "fs-extra": "^10.1.0" + "fs-extra": "^10.1.0", + "pg": "^8.8.0", + "typeorm": "^0.3.10" } } diff --git a/src/client/index.ts b/src/client/index.ts index 68199fb..52ad420 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -15,4 +15,6 @@ client.on("ready", () => { console.log(`Connected to Discord API. Bot account is ${client.user?.tag} (${client.user?.id})`); }); -export default client; \ No newline at end of file +export default client; + +import "../commands"; \ No newline at end of file diff --git a/src/commands/ci.ts b/src/commands/ci.ts new file mode 100644 index 0000000..aee9a24 --- /dev/null +++ b/src/commands/ci.ts @@ -0,0 +1,7 @@ +import * as notification from "./notification"; + +const array = [notification.builder.toJSON()]; + +export { + array +} \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index e69de29..e34d322 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -0,0 +1,39 @@ +import { ChatInputCommandInteraction, Collection, Events, SlashCommandBuilder } from "discord.js"; +import * as notification from "./notification"; +import client from "../client"; +import getDefaultEmbed from "../tools/defaultEmbeds"; + +const commands = new Collection Promise }>(); +commands.set(notification.builder.name, notification); + +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + const command = commands.get(interaction.commandName); + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found`); + return; + } + + try { + await command.execute(interaction); + } catch (e) { + + if (e instanceof Error && "stack" in e) { + console.error(e.stack); + } else { + console.error(e) + } + + + const embed = getDefaultEmbed(); + embed.setTitle("Something went wrong"); + embed.setDescription("An unexpected error occurred while processing your command. Please contact support if the problem persists"); + embed.setColor(0xD01B15); + await interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); + } +}); \ No newline at end of file diff --git a/src/commands/notification.ts b/src/commands/notification.ts index 76d1884..49bfa48 100644 --- a/src/commands/notification.ts +++ b/src/commands/notification.ts @@ -1,5 +1,7 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ChatInputCommandInteraction, NewsChannel, TextBasedChannel, CategoryChannel, StageChannel, TextChannel, PrivateThreadChannel, PublicThreadChannel, VoiceChannel, APIInteractionDataResolvedChannel, ForumChannel } from "discord.js"; -import { getSuccessEmbed } from "../tools/defaultEmbeds"; +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ChatInputCommandInteraction, NewsChannel, TextBasedChannel, CategoryChannel, StageChannel, TextChannel, PrivateThreadChannel, PublicThreadChannel, VoiceChannel, APIInteractionDataResolvedChannel, ForumChannel, GuildBasedChannel } from "discord.js"; +import getDefaultEmbed, { getSuccessEmbed } from "../tools/defaultEmbeds"; +import { database, GuildSetting } from "../data"; +import client from "../client"; const builder = new SlashCommandBuilder(); builder.setName("logchannel"); @@ -8,37 +10,119 @@ builder.setDMPermission(false); builder.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels); builder.addChannelOption((option) => { option.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement); - option.setName("Channel"); + option.setName("channel"); option.setDescription("The channel to send notifications to"); - option.setRequired(true); + option.setRequired(false); return option; }); -const execute = (interaction: ChatInputCommandInteraction) => { - const channel:TextBasedChannel = getTextBasedChannel(interaction.options.getChannel("Channel", true)); - - //TODO Save channel in config - - const embed = getSuccessEmbed(); - embed.setDescription(`Log channel has been set to <#${channel.id}>`); - interaction.reply({ - embeds: [embed], - ephemeral: true +async function getGuildSetting(guildID: string): Promise { + let guildSetting = await database.getRepository(GuildSetting).findOne({ + where: { + id: guildID + } }); - const logMessage = getSuccessEmbed(); - logMessage.setDescription("This channel has been selected as the log channel"); - logMessage.addFields({ + 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; + + guildSetting.notificationChannelID = null; + await database.getRepository(GuildSetting).save(guildSetting); + + const logEmbed = getDefaultEmbed(); + logEmbed.setTitle("Settings changed"); + logEmbed.setDescription("Log channel has been disabled"); + logEmbed.addFields({ name: "This action was performed by", value: `${interaction.user.tag} (${interaction.user.id})` }); - channel.send({ - embeds: [logMessage] + + if (logChannel && logChannel.isTextBased()) { + logChannel.send({ + embeds: [logEmbed] + }).catch(); + } + + const embed = getSuccessEmbed(); + embed.setDescription("Log channel has been disabled"); + interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); +} + +const execute = async (interaction: ChatInputCommandInteraction) => { + if (!interaction.guildId) throw new Error("Command can only be used inside a guild"); + + const optionVal = interaction.options.getChannel("channel", false); + const guildSetting = await getGuildSetting(interaction.guildId); + + if (!optionVal) return await resetNotificationChannel(guildSetting, interaction); + + const channel = getTextBasedChannel(optionVal); + if (guildSetting.notificationChannelID) { + const oldLogChannel = await getGuildChannel(guildSetting.id, guildSetting.notificationChannelID); + const embed = getDefaultEmbed(); + embed.setTitle("Settings changed"); + embed.setDescription(`Log channel has been switched to <#${channel.id}>`); + embed.addFields({ + name: "This action was performed by", + value: `${interaction.user.tag} (${interaction.user.id})` + }); + + if (oldLogChannel && oldLogChannel.isTextBased()) { + oldLogChannel.send({ + embeds: [embed] + }).catch(); + } + } + + guildSetting.notificationChannelID = channel.id; + await database.getRepository(GuildSetting).save(guildSetting); + + const embed = getDefaultEmbed(); + embed.setTitle("Settings changed"); + embed.setDescription("This channel has been set as the log channel"); + embed.addFields({ + name: "This action was performed by", + value: `${interaction.user.tag} (${interaction.user.id})` }); + + channel.send({ + embeds: [ embed ] + }).catch(); + + const reply = getSuccessEmbed(); + reply.setDescription(`Log channel was set to <#${channel.id}>`); + + interaction.reply({ + embeds: [ reply ], + ephemeral: true + }).catch(); + + return; } function getTextBasedChannel(channel: CategoryChannel | NewsChannel | StageChannel | TextChannel | PrivateThreadChannel | PublicThreadChannel | VoiceChannel | ForumChannel | APIInteractionDataResolvedChannel): TextBasedChannel { - if (channel.type === ChannelType.DM || channel.type === ChannelType.GroupDM || channel.type === ChannelType.GuildAnnouncement || channel.type === ChannelType.GuildText || channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread || channel.type === ChannelType.GuildVoice) { + if (channel.type === ChannelType.GuildAnnouncement || channel.type === ChannelType.GuildText || channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread || channel.type === ChannelType.GuildVoice) { return channel as TextBasedChannel; } diff --git a/src/data/dataSource.ts b/src/data/dataSource.ts new file mode 100644 index 0000000..c7613f1 --- /dev/null +++ b/src/data/dataSource.ts @@ -0,0 +1,31 @@ +import path from "path"; +import { DataSource } from "typeorm"; +import { config } from "dotenv"; +import { Badword, GuildSetting } from "./model"; + +config(); + +const host = process.env["DB_HOST"]; +const username = process.env["DB_USERNAME"]; +const password = process.env["DB_PASSWORD"]; +const database = process.env["DB_DATABASE"]; + +if (!host) throw new ReferenceError("Environment variable DB_HOST is missing"); +if (!username) throw new ReferenceError("Environment variable DB_USERNAME is missing"); +if (!password) throw new ReferenceError("Environment variable DB_PASSWORD is missing"); +if (!database) throw new ReferenceError("Environment variable DB_DATABASE is missing"); + +const dataSource = new DataSource({ + type: "postgres", + host: host, + port: 5432, + username: username, + password: password, + database: database, + migrationsRun: true, + migrations: [ path.join(__dirname, "/migrations/*") ], + entities: [Badword,GuildSetting], + migrationsTransactionMode: "each" +}); + +export default dataSource; \ No newline at end of file diff --git a/src/data/index.ts b/src/data/index.ts new file mode 100644 index 0000000..3440bd5 --- /dev/null +++ b/src/data/index.ts @@ -0,0 +1,9 @@ +import dataSource from "./dataSource"; +import { Badword, GuildSetting } from "./model"; + +export { + dataSource as database, + Badword, + GuildSetting +} + diff --git a/src/data/migrations/1669251793386-data.ts b/src/data/migrations/1669251793386-data.ts new file mode 100644 index 0000000..7dd5712 --- /dev/null +++ b/src/data/migrations/1669251793386-data.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class data1669251793386 implements MigrationInterface { + name = 'data1669251793386' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "badword" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "guildID" character varying, + "value" character varying NOT NULL, + CONSTRAINT "PK_b5034b5fcec4ccac0c288e37f3a" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + CREATE TABLE "guild_setting" ( + "id" character varying NOT NULL, + "notificationChannelID" character varying, + "isPremiumUntil" date, + CONSTRAINT "PK_56f0d706a92e999b4e967abae5f" PRIMARY KEY ("id") + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP TABLE "guild_setting" + `); + await queryRunner.query(` + DROP TABLE "badword" + `); + } + +} diff --git a/src/data/model/badword.ts b/src/data/model/badword.ts new file mode 100644 index 0000000..28462f6 --- /dev/null +++ b/src/data/model/badword.ts @@ -0,0 +1,13 @@ +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; + +@Entity() +export class Badword { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column("varchar", { nullable: true }) + guildID!: string | null; + + @Column("varchar") + value!: string; +} \ No newline at end of file diff --git a/src/data/model/guildSetting.ts b/src/data/model/guildSetting.ts new file mode 100644 index 0000000..9ccca0f --- /dev/null +++ b/src/data/model/guildSetting.ts @@ -0,0 +1,13 @@ +import { Entity, PrimaryColumn, Column } from "typeorm"; + +@Entity() +export class GuildSetting { + @PrimaryColumn("varchar") + id!: string; + + @Column("varchar", { nullable: true, default: null }) + notificationChannelID!: string | null; + + @Column("date", { nullable: true, default: null }) + isPremiumUntil!: Date | null; +} \ No newline at end of file diff --git a/src/data/model/index.ts b/src/data/model/index.ts new file mode 100644 index 0000000..81353be --- /dev/null +++ b/src/data/model/index.ts @@ -0,0 +1,7 @@ +import { Badword } from "./badword"; +import { GuildSetting } from "./guildSetting"; + +export { + Badword, + GuildSetting +} \ No newline at end of file diff --git a/src/tools/consoleSwapper.ts b/src/tools/consoleSwapper.ts index c9d7a46..c037dc9 100644 --- a/src/tools/consoleSwapper.ts +++ b/src/tools/consoleSwapper.ts @@ -1,24 +1,25 @@ /* eslint-disable prefer-rest-params */ /** * This module swaps the default console outputs to own functions and adds a timestamp to each message + * (c) 2022 AstroGD */ import path from "path"; import fs from "fs-extra"; export default function() { - fs.ensureDirSync(path.join(__dirname, "../data/log/")); + fs.ensureDirSync(path.join(__dirname, "../../data/logs/")); const callDate = new Date(); const logFileName = `${callDate.getUTCFullYear()}-${`0${callDate.getUTCMonth() + 1}`.slice(-2)}-${`0${callDate.getUTCDate()}`.slice(-2)}-${`0${callDate.getUTCHours()}`.slice(-2)}-${`0${callDate.getUTCMinutes()}`.slice(-2)}-${`0${callDate.getUTCSeconds()}`.slice(-2)}-${`00${callDate.getUTCMilliseconds()}`.slice(-3)}.log`; - const logFile = fs.createWriteStream(path.join(__dirname, `../data/log/${logFileName}`)); + const logFile = fs.createWriteStream(path.join(__dirname, `../../data/logs/${logFileName}`)); const out = new console.Console(process.stdout, process.stderr, true); const logConsole = new console.Console(logFile); function log() { const now = new Date(); const Prepend = `[i] [${now.getUTCFullYear()}-${`0${now.getUTCMonth() + 1}`.slice(-2)}-${`0${now.getUTCDate()}`.slice(-2)} ${`0${now.getUTCHours()}`.slice(-2)}:${`0${now.getUTCMinutes()}`.slice(-2)}:${`0${now.getUTCSeconds()}`.slice(-2)}.${`00${now.getUTCMilliseconds()}`.slice(-3)}] `; - arguments[0] = `${Prepend}${arguments[0].toString().normalize()}`; + arguments[0] = `${Prepend}${arguments[0]?.toString().normalize()}`; for (let i = 0; i < arguments.length; i++) { arguments[i] = arguments[i].split("\n").join(`\n${new Array(31).join(" ")}`); @@ -31,10 +32,10 @@ function warn() { const now = new Date(); const Prepend = `[W] [${now.getUTCFullYear()}-${`0${now.getUTCMonth() + 1}`.slice(-2)}-${`0${now.getUTCDate()}`.slice(-2)} ${`0${now.getUTCHours()}`.slice(-2)}:${`0${now.getUTCMinutes()}`.slice(-2)}:${`0${now.getUTCSeconds()}`.slice(-2)}.${`00${now.getUTCMilliseconds()}`.slice(-3)}] `; - arguments[0] = `${Prepend}${arguments[0].toString().normalize()}`; + arguments[0] = `${Prepend}${arguments[0]?.toString().normalize()}`; for (let i = 0; i < arguments.length; i++) { - const argument = arguments[i].toString().normalize(); + const argument = arguments[i]?.toString().normalize(); arguments[i] = argument.split("\n").join(`\n${new Array(31).join(" ")}`); } @@ -45,10 +46,10 @@ function error() { const now = new Date(); const Prepend = `==== [ERROR] [${now.getUTCFullYear()}-${`0${now.getUTCMonth() + 1}`.slice(-2)}-${`0${now.getUTCDate()}`.slice(-2)} ${`0${now.getUTCHours()}`.slice(-2)}:${`0${now.getUTCMinutes()}`.slice(-2)}:${`0${now.getUTCSeconds()}`.slice(-2)}.${`00${now.getUTCMilliseconds()}`.slice(-3)}] ====\n`; - arguments[0] = `${Prepend}${arguments[0].toString().normalize()}`; + arguments[0] = `${Prepend}${arguments[0]?.toString().normalize()}`; for (let i = 0; i < arguments.length; i++) { - arguments[i] = arguments[i].toString().normalize(); + arguments[i] = arguments[i]?.toString().normalize(); } out.error(...arguments); diff --git a/tsconfig.json b/tsconfig.json index c6c0127..1d6a364 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ @@ -47,21 +47,21 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./build", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ + "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ @@ -99,5 +99,9 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": [ + "index.ts", + "src" + ] } From 082bfe6097e78b69cc8d501fdd352423491efe96 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 12:12:50 +0100 Subject: [PATCH 03/10] logchannel command --- docker-compose.yml | 6 +++--- index.ts | 5 ++++- package-lock.json | 16 ++++++++++++++++ package.json | 1 + src/client/index.ts | 13 +------------ src/client/init.ts | 10 ++++++++++ src/data/dataSource.ts | 2 ++ tsconfig.json | 2 +- 8 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 src/client/init.ts diff --git a/docker-compose.yml b/docker-compose.yml index 263a65c..60319fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,16 +5,16 @@ services: build: ./ depends_on: - database - restart: always + restart: unless-stopped environment: - TOKEN=$TOKEN - - DB_HOST=database:5432 + - DB_HOST=database - DB_USERNAME=$DB_USERNAME - DB_PASSWORD=$DB_PASSWORD - DB_DATABASE=$DB_DATABASE database: image: postgres:latest - restart: always + restart: unless-stopped ports: - 5432:5432 environment: diff --git a/index.ts b/index.ts index 0929e98..ce68056 100644 --- a/index.ts +++ b/index.ts @@ -4,10 +4,13 @@ import swapConsole from "./src/tools/consoleSwapper"; config(); swapConsole(); -import "./src/client"; +import "./src/client/init"; +import "./src/commands"; +import client from "./src/client"; function shutdown() { console.log("Shutdown request received"); + if (client) client.destroy(); process.exit(); } diff --git a/package-lock.json b/package-lock.json index c0f745d..f088f63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "devDependencies": { "@types/fs-extra": "^9.0.13", "@types/node": "^18.11.9", + "rimraf": "^3.0.2", "shx": "^0.3.4", "ts-node": "^10.9.1", "typescript": "^4.9.3" @@ -1029,6 +1030,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/package.json b/package.json index 2b75eb6..1be8d2e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@types/fs-extra": "^9.0.13", "@types/node": "^18.11.9", + "rimraf": "^3.0.2", "shx": "^0.3.4", "ts-node": "^10.9.1", "typescript": "^4.9.3" diff --git a/src/client/index.ts b/src/client/index.ts index 52ad420..c8aa925 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -6,15 +6,4 @@ const client = new Client({ ] }); -const token = process.env["TOKEN"]; -if (!token) throw new ReferenceError("TOKEN environment variable is missing"); - -client.login(token); - -client.on("ready", () => { - console.log(`Connected to Discord API. Bot account is ${client.user?.tag} (${client.user?.id})`); -}); - -export default client; - -import "../commands"; \ No newline at end of file +export default client; \ No newline at end of file diff --git a/src/client/init.ts b/src/client/init.ts new file mode 100644 index 0000000..bfa5e6e --- /dev/null +++ b/src/client/init.ts @@ -0,0 +1,10 @@ +import client from "./index"; + +const token = process.env["TOKEN"]; +if (!token) throw new ReferenceError("TOKEN environment variable is missing"); + +client.login(token); + +client.on("ready", () => { + console.log(`Connected to Discord API. Bot account is ${client.user?.tag} (${client.user?.id})`); +}); \ No newline at end of file diff --git a/src/data/dataSource.ts b/src/data/dataSource.ts index c7613f1..4dde07a 100644 --- a/src/data/dataSource.ts +++ b/src/data/dataSource.ts @@ -28,4 +28,6 @@ const dataSource = new DataSource({ migrationsTransactionMode: "each" }); +dataSource.initialize(); + export default dataSource; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1d6a364..dacf7c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ From 317da97cd36e8c75917c5d1593a55d68389d09ad Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 12:29:52 +0100 Subject: [PATCH 04/10] Fix CI, update Readme --- Dockerfile | 2 +- README.md | 4 ++++ docker-compose.yml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 144ce89..d54b947 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:18-alpine AS builder ENV NODE_ENV=production +# RUN apk add --no-cache python3 make g++ COPY build/package*.json ./ -RUN apk add --no-cache python3 make g++ RUN npm install --omit=dev FROM node:18-alpine AS app diff --git a/README.md b/README.md index ea032b8..717bc17 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # eu.astrogd.white-leopard A Discord bot that checks Discord channel names for banned words and prevents renaming of them +## Commands +### /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. + ## Environment variables | Name | Description | Required | Example | | :---------- | :------------------------------------------------------------ | :------: | :------------------ | diff --git a/docker-compose.yml b/docker-compose.yml index 60319fa..e34b63e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "2" services: app: - image: astrogd/white-leopard + image: astrogd/white-leopard:dev build: ./ depends_on: - database From a26fb2df1251a848cf1dcdd4452d2eea9be4e6b2 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 15:36:33 +0100 Subject: [PATCH 05/10] /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 From def44d27746b9946ee8ea8191666a860427c1525 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 16:25:20 +0100 Subject: [PATCH 06/10] /info --- package.json | 2 +- src/commands/blocklist.ts | 9 ++++-- src/commands/ci.ts | 3 +- src/commands/index.ts | 2 ++ src/commands/info.ts | 66 ++++++++++++++++++++++++++++++++++++++ src/tools/data.ts | 3 -- src/tools/defaultEmbeds.ts | 11 ++++--- src/tools/design.ts | 30 +++++++++++++++++ 8 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 src/commands/info.ts create mode 100644 src/tools/design.ts diff --git a/package.json b/package.json index ec74e5f..8c25db0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.2", + "version": "1.0.0-alpha.3", "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 index ba99b95..a90b102 100644 --- a/src/commands/blocklist.ts +++ b/src/commands/blocklist.ts @@ -4,6 +4,7 @@ import { IsNull } from "typeorm"; import { getGuildSetting, isPremiumActive } from "../tools/data"; import getDefaultEmbed, { getFailedEmbed, getSuccessEmbed } from "../tools/defaultEmbeds"; import { getGuildChannel } from "../tools/discord"; +import { Color, Emoji } from "../tools/design"; const builder = new SlashCommandBuilder(); builder.setName("blocklist"); @@ -81,7 +82,7 @@ async function execute(interaction: ChatInputCommandInteraction): Promise 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"); + 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 @@ -122,7 +123,8 @@ async function execute(interaction: ChatInputCommandInteraction): Promise }); const logMessage = getDefaultEmbed(); - logMessage.setTitle("Word added"); + logMessage.setTitle(`${Emoji.SETTINGS} Word added`); + logMessage.setColor(Color.INFORMING_BLUE); logMessage.setDescription(`"||${word}||" has been added to the blocklist`); logMessage.addFields({ name: "This action was performed by", @@ -168,7 +170,8 @@ async function execute(interaction: ChatInputCommandInteraction): Promise }).catch(); const logMessage = getDefaultEmbed(); - logMessage.setTitle("Word removed"); + logMessage.setTitle(`${Emoji.SETTINGS} Word removed`); + logMessage.setColor(Color.INFORMING_BLUE); logMessage.setDescription(`"||${word}||" has been removed from the blocklist`); logMessage.addFields({ name: "This action was performed by", diff --git a/src/commands/ci.ts b/src/commands/ci.ts index 2216d4b..4372998 100644 --- a/src/commands/ci.ts +++ b/src/commands/ci.ts @@ -1,7 +1,8 @@ import * as notification from "./notification"; import * as blocklist from "./blocklist"; +import * as info from "./info"; -const array = [notification.builder.toJSON(), blocklist.builder.toJSON()]; +const array = [notification.builder.toJSON(), blocklist.builder.toJSON(), info.builder.toJSON()]; export { array diff --git a/src/commands/index.ts b/src/commands/index.ts index b2a3908..ce8064a 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,12 +1,14 @@ import { ChatInputCommandInteraction, Collection, Events, SlashCommandBuilder } from "discord.js"; import * as notification from "./notification"; import * as blocklist from "./blocklist"; +import * as info from "./info"; 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); +commands.set(info.builder.name, info); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; diff --git a/src/commands/info.ts b/src/commands/info.ts new file mode 100644 index 0000000..5d39176 --- /dev/null +++ b/src/commands/info.ts @@ -0,0 +1,66 @@ +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"; + +const builder = new SlashCommandBuilder(); +builder.setName("info"); +builder.setDescription("Shows information about this bot and the server settings"); +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() + } + }); + const localBlockedWordsCount = await database.getRepository(Badword).count({ + where: { + guildID: interaction.guildId + } + }); + + 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.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(), + inline: true + },{ + name: "Local word count", + value: localBlockedWordsCount.toString(), + inline: true + },{ + name: `${Emoji.WAVING} Have a question or want to say hello?`, + value: "Join the support Discord server at https://go.astrogd.eu/discord" + }); + + interaction.reply({ + embeds: [embed], + ephemeral: true + }).catch(); +} + +export { + builder, + execute +} \ No newline at end of file diff --git a/src/tools/data.ts b/src/tools/data.ts index 11b5b30..a12e1ee 100644 --- a/src/tools/data.ts +++ b/src/tools/data.ts @@ -18,9 +18,6 @@ export async function getGuildSetting(guildID: string): Promise { } export function isPremiumActive(timestamp: Date | null): boolean { - - console.log(timestamp); - if (timestamp === null) return false; const now = Number(new Date()); const activeUntil = Number(timestamp); diff --git a/src/tools/defaultEmbeds.ts b/src/tools/defaultEmbeds.ts index 7004af2..8c916a6 100644 --- a/src/tools/defaultEmbeds.ts +++ b/src/tools/defaultEmbeds.ts @@ -1,6 +1,7 @@ import pack from "../../package.json"; import { EmbedBuilder } from "discord.js"; import client from "../client"; +import { Color, Emoji } from "./design"; // const _coolColors = [0x054566]; @@ -11,21 +12,21 @@ export default function getDefaultEmbed(): EmbedBuilder { iconURL: client.user?.avatarURL() || undefined, }); embed.setTimestamp(new Date()); - embed.setColor(0x3682cc); + embed.setColor(Color.ANONYMOUS_GRAY); return embed; } export function getSuccessEmbed(): EmbedBuilder { const embed = getDefaultEmbed(); - embed.setTitle("Success"); - embed.setColor(0x32d122); + embed.setTitle(`${Emoji.CHAT_APPROVE} Success`); + embed.setColor(Color.SUCCESSFUL_GREEN); return embed; } export function getFailedEmbed(): EmbedBuilder { const embed = getDefaultEmbed(); - embed.setTitle("Failed"); - embed.setColor(0xD01B15); + embed.setTitle(`${Emoji.CHAT_DENY} Failed`); + embed.setColor(Color.STOPSIGN_RED); return embed; } \ No newline at end of file diff --git a/src/tools/design.ts b/src/tools/design.ts new file mode 100644 index 0000000..4a5be44 --- /dev/null +++ b/src/tools/design.ts @@ -0,0 +1,30 @@ +export enum Emoji { + DOUBLE_ARROW_RIGHT = "<:double_arrow_right:918922668936413267>", + SETTINGS = "<:settings:918912063970099232>", + TIME = "<:time:918913616743387156>", + DOCS = "<:docs:918917779283918899>", + MESSAGE = "<:message:918920872683786280>", + WAVING = "<:waving:918949572804505640>", + CHAT_APPROVE = "<:chat_approve:918910607317667860>", + CHAT_DENY = "<:chat_deny:918911411663544410>", + WARN = "<:warn:918914600181825556>", + INFORMATION = "<:information:918912973874028614>", + ERROR = "<:error:918915254447136841>", + SWITCH_ON = "<:switch_on:918915977662586892>", + SWITCH_OFF = "<:switch_off:918917065899925584>", + SWITCH_UNSET = "<:switch_unset:918917082807156796>", + SECURITY_CHALLENGE = "<:security_challenge:918919903405305946>", + SECURITY_CHALLENGE_SUCCESS = "<:security_challenge_success:918919918672576562>", + SECURITY_CHALLENGE_FAILED = "<:security_challenge_failed:918919932887064696>", + ASTROGD = "<:astrogd:918906741125697626>", + PREMIUM = "<:premium:918909591255908442>", +} + +export enum Color { + PREMIUM_ORANGE = 0xFFC800, + SUCCESSFUL_GREEN = 0x77DE37, + STOPSIGN_RED = 0xDA2132, + WARNING_YELLOW = 0xF0E210, + INFORMING_BLUE = 0x2FAAE2, + ANONYMOUS_GRAY = 0x7B7B7B +} \ No newline at end of file From dc6755eb198fa521e253ffc16d6e9b06ecae8761 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 17:27:56 +0100 Subject: [PATCH 07/10] censoring --- index.ts | 1 + package.json | 2 +- src/events/channelUpdate.ts | 85 +++++++++++++++++++++++++++++++++++++ src/events/index.ts | 1 + 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/events/channelUpdate.ts create mode 100644 src/events/index.ts diff --git a/index.ts b/index.ts index ce68056..1775a40 100644 --- a/index.ts +++ b/index.ts @@ -6,6 +6,7 @@ swapConsole(); import "./src/client/init"; import "./src/commands"; +import "./src/events"; import client from "./src/client"; function shutdown() { diff --git a/package.json b/package.json index 8c25db0..a6811c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.4", "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/events/channelUpdate.ts b/src/events/channelUpdate.ts new file mode 100644 index 0000000..9d41d2b --- /dev/null +++ b/src/events/channelUpdate.ts @@ -0,0 +1,85 @@ +import client from "../client"; +import { Events } from "discord.js"; +import { getGuildSetting, isPremiumActive } 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.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: { + 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; + + 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`); + } 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({ + name: "Detected banned word:", + value: `||${found}||` + },{ + name: "Old channel name:", + value: `||${name}||`, + inline: true + }); + + 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.id}> (${newChannel.id}) has been renamed because its name contained a blocked word.`); + embed.setColor(Color.WARNING_YELLOW); + if (isPremium) embed.addFields({ + name: "Detected banned word:", + value: `||${found}||`, + inline: true + },{ + name: "Old channel name:", + value: `||${name}||`, + inline: true + }); + + logChannel.send({ + embeds: [embed] + }).catch(); +}); \ No newline at end of file diff --git a/src/events/index.ts b/src/events/index.ts new file mode 100644 index 0000000..f427567 --- /dev/null +++ b/src/events/index.ts @@ -0,0 +1 @@ +import "./channelUpdate"; \ No newline at end of file From 62aa05031f4d8d9d9a11204cf1dc4f600b982200 Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 18:07:29 +0100 Subject: [PATCH 08/10] Update README --- README.md | 3 +++ package.json | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5e0825b..cc5822f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Adds the word to the server specific blocklist #### /blocklist remove [word] Removes the word from the server specific blocklist +### /info +Returns general information about the bot and the servers stats + ## Environment variables | Name | Description | Required | Example | | :---------- | :------------------------------------------------------------ | :------: | :------------------ | diff --git a/package.json b/package.json index a6811c9..ece09f5 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.4", + "version": "1.0.0-alpha.5", "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", - "deploy-dev": "ts-node ci/devDeploy.ts", - "start": "npm run build && npm run deploy-dev && docker-compose up --no-start && docker compose start database && 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", + "start": "npm run build && npm run deploy-commands:dev && docker-compose up --no-start && docker compose start database && node --enable-source-maps .", "migration:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d src/data/dataSource.ts -p src/data/migrations/data", "migration:run": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:run -d src/data/dataSource.ts", "migration:revert": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:revert -d src/data/dataSource.ts", From cd60d7dabcdecca8fce0d823229fa89cdcc6df3e Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 20:15:36 +0100 Subject: [PATCH 09/10] cli support --- docker-compose.yml | 4 +- index.ts | 4 +- package-lock.json | 13 +++- package.json | 1 + src/cli/global.ts | 80 ++++++++++++++++++++++++ src/cli/guild.ts | 150 +++++++++++++++++++++++++++++++++++++++++++++ src/cli/index.ts | 68 ++++++++++++++++++++ 7 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 src/cli/global.ts create mode 100644 src/cli/guild.ts create mode 100644 src/cli/index.ts diff --git a/docker-compose.yml b/docker-compose.yml index e34b63e..d1e3707 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,10 @@ -version: "2" +version: "3.9" services: app: image: astrogd/white-leopard:dev build: ./ + tty: true + stdin_open: true depends_on: - database restart: unless-stopped diff --git a/index.ts b/index.ts index 1775a40..c430e4a 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,7 @@ swapConsole(); import "./src/client/init"; import "./src/commands"; import "./src/events"; +import "./src/cli"; import client from "./src/client"; function shutdown() { @@ -16,4 +17,5 @@ function shutdown() { } process.on("SIGINT", shutdown); -process.on("SIGHUP", shutdown); \ No newline at end of file +process.on("SIGHUP", shutdown); +process.on("SIGTERM", shutdown); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f088f63..8bad6c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.5", "license": "CC-BY-NC-ND-4.0", "dependencies": { "discord.js": "^14.6.0", "dotenv": "^16.0.3", "fs-extra": "^10.1.0", + "moment": "^2.29.4", "pg": "^8.8.0", "typeorm": "^0.3.10" }, @@ -771,6 +772,14 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index ece09f5..f5b1047 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "discord.js": "^14.6.0", "dotenv": "^16.0.3", "fs-extra": "^10.1.0", + "moment": "^2.29.4", "pg": "^8.8.0", "typeorm": "^0.3.10" } diff --git a/src/cli/global.ts b/src/cli/global.ts new file mode 100644 index 0000000..3ca2376 --- /dev/null +++ b/src/cli/global.ts @@ -0,0 +1,80 @@ +import { IsNull } from "typeorm"; +import { Badword, database } from "../data"; +import { Console } from "console"; + +const console = new Console(process.stdout); + +export default async function execute(args: string[]) { + const command = args[0]; + if (!command) return printHelp(); + + switch (command.toLowerCase()) { + case "get": { + const globalWords = await database.getRepository(Badword).find({ + where: { + guildID: IsNull() + } + }); + + console.log(`Global blocked words:\n\n${globalWords.map(w => w.value).reduce((c, n) => c + ", " + n, "").slice(2)}`); + break; + } + + case "add": { + const word = args[1]?.toLowerCase(); + if (!word) return printHelp(); + + if (await database.getRepository(Badword).count({ + where: { + value: word, + guildID: IsNull() + } + }) > 0) return console.log(`${word} is already in the blocklist`); + + const entity = new Badword(); + entity.value = word; + + await database.getRepository(Badword).save(entity); + console.log(`${word} has been added to the global block list`); + break; + } + + case "remove": { + const word = args[1]?.toLowerCase(); + if (!word) return printHelp(); + + await database.getRepository(Badword).delete({ + value: word, + guildID: IsNull() + }); + + console.log(`Removed ${word} from the global block list`); + break; + } + + case "count": { + const count = await database.getRepository(Badword).count({ + where: { + guildID: IsNull() + } + }); + + console.log(`There are ${count} globally blocked words`); + break; + } + + default: { + printHelp(); + break; + } + } +} + +function printHelp() { + console.log(`Usage "global": + +global get +global add [WORD] +global remove [WORD] +global count`); +} \ No newline at end of file diff --git a/src/cli/guild.ts b/src/cli/guild.ts new file mode 100644 index 0000000..9c98555 --- /dev/null +++ b/src/cli/guild.ts @@ -0,0 +1,150 @@ +import { getGuildSetting, isPremiumActive } from "../tools/data"; +import moment from "moment"; +import { Badword, database, GuildSetting } from "../data"; +import { Console } from "console"; + +const console = new Console(process.stdout); + +export default async function execute(args: string[]) { + const command = args[0]; + if (!command) return printHelp(); + + switch (command.toLowerCase()) { + 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] + } + }); + + console.log(`Guild ${args[1]}: + - Premium: ${isPremium ? `ACTIVE for ${moment(settings.isPremiumUntil).fromNow(true)}` : "INACTIVE"} + - 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(); + + const badWords = await database.getRepository(Badword).find({ + where: { + guildID: args[2] + } + }); + + switch (args[1].toLowerCase()) { + case "get": { + console.log(`Bad words for ${args[2]}:\n\n${badWords.map((badWord) => badWord.value).reduce((prev, next) => prev + ", " + next, "").slice(2)}`); + break; + } + + case "add": { + if (!args[3]) { + printWordHelp(); + break; + } + + if (badWords.filter(w => w.value === args[3]!.toLowerCase()).length > 0) { + console.log("Word already exists"); + break; + } + + const badWord = new Badword(); + badWord.guildID = args[2]; + badWord.value = args[3].toLowerCase(); + + await database.getRepository(Badword).save(badWord); + console.log(`${args[3].toLowerCase()} added to guild ${args[2]}`); + break; + } + + case "remove": { + if (!args[3]) { + printWordHelp(); + break; + } + + const badWord = badWords.find((w) => w.value === args[3]?.toLowerCase()); + + if (!badWord) { + console.log(`${args[3].toLowerCase()} is not in blocklist of guild ${args[2]}`); + break; + } + + await database.getRepository(Badword).delete({ + id: badWord.id + }); + + console.log(`${badWord.value} deleted for guild ${args[2]}`); + break; + } + + case "clear": { + await database.getRepository(Badword).delete({ + guildID: args[2] + }); + + console.log(`Deleted ${badWords.length} entries`); + break; + } + + default: { + printHelp(); + break; + } + } + + break; + } + + default: { + printHelp(); + break; + } + } +} + + +function printHelp() { + console.log(`Usage "guild": + +guild info [GUILDID] +guild setPremium [GUILDID] [YYYY-MM-DD or NULL] +guild words [get|add|remove|clear]`); +} + +function printWordHelp() { + console.log(`Usage "guild words": + +guild words get [GUILDID] +guild words add [GUILDID] [WORD] +guild words remove [GUILDID] [WORD] +guild words clear [GUILDID]`); +} \ No newline at end of file diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..54fb3d2 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,68 @@ +import * as readline from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; +import pack from "../../package.json"; +import { Console } from "node:console"; +import moment from "moment"; + +import guild from "./guild"; +import global from "./global"; + +const console = new Console(process.stdout); + +const startupTime = new Date(); +const rl = readline.createInterface(input, output); + +rl.on("line", async (msg) => { + const [command, ...args] = msg.split(" "); + if (!command) return; + + switch (command.toLowerCase()) { + case "version": { + console.log(`Channel filter V${pack.version} by AstroGD®`); + break; + } + + case "uptime": { + console.log(`Application is running for ${moment(startupTime).fromNow(true)}`); + break; + } + + case "clear": { + console.clear(); + break; + } + + case "guild": { + await guild(args); + break; + } + + case "help": { + printHelp(); + break; + } + + case "global": { + await global(args); + break; + } + + default: { + console.log(`Unknown command. Try "help" for help`); + break; + } + } + + rl.prompt(); +}); + +function printHelp() { + console.log(`Commands: + +version +uptime +guild +global +help +clear`); +} From f8d029431f7545b134356480432a4ff1578f170e Mon Sep 17 00:00:00 2001 From: Lukas | AstroGD Date: Thu, 24 Nov 2022 21:25:41 +0100 Subject: [PATCH 10/10] Beta release --- README.md | 18 ++++++++++-------- ci/deploy.ts | 4 ++-- package-lock.json | 4 ++-- package.json | 3 ++- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cc5822f..196842c 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,16 @@ Removes the word from the server specific blocklist Returns general information about the bot and the servers stats ## Environment variables -| Name | Description | Required | Example | -| :---------- | :------------------------------------------------------------ | :------: | :------------------ | -| TOKEN | Discord bot token to log into the API with | ▶️/🚀 | NzYzMDP3MzE1Mzky... | -| DB_HOST | Hostname of the database | ▶️ | 127.0.0.1:3546 | -| DB_USERNAME | Username of the database | ▶️ | root | -| DB_PASSWORD | Password of the database | ▶️ | abc123 | -| DB_DATABASE | Database name | ▶️ | white-leopard | -| CLIENT_ID | Client ID of the Discord appication associated with the token | 🚀 | 763035392692274 | +| Name | Description | Required | Example | +| :--------------- | :------------------------------------------------------------------------------ | :------: | :------------------ | +| TOKEN | Discord bot token to log into the API with | ▶️/🚀 | NzYzMDP3MzE1Mzky... | +| DB_HOST | Hostname of the database | ▶️ | 127.0.0.1:3546 | +| DB_USERNAME | Username of the database | ▶️ | root | +| DB_PASSWORD | Password of the database | ▶️ | abc123 | +| DB_DATABASE | Database name | ▶️ | white-leopard | +| CLIENT_ID | Client ID of the Discord appication associated with the token | 🚀 | 763035392692274 | +| DEPLOY_TOKEN | Production Discord bot token to log into the API with | 🚀 | NzYzMDP3MzE1Mzky... | +| DEPLOY_CLIENT_ID | Production Client ID of the Discord appication associated with the deploy token | 🚀 | 763035392692274 | ### Icon explanation: - ▶️ = Required in runtime diff --git a/ci/deploy.ts b/ci/deploy.ts index f454013..9ece94b 100644 --- a/ci/deploy.ts +++ b/ci/deploy.ts @@ -4,8 +4,8 @@ import dotenv from "dotenv"; dotenv.config({ path: path.join(__dirname, "../.env") }); // Environment checking -const TOKEN = process.env["TOKEN"]; -const CLIENT_ID = process.env["CLIENT_ID"]; +const TOKEN = process.env["DEPLOY_TOKEN"]; +const CLIENT_ID = process.env["DEPLOY_CLIENT_ID"]; if (!TOKEN) throw new ReferenceError("Environment variable TOKEN is missing"); if (!CLIENT_ID) throw new ReferenceError("Environment variable CLIENT_ID is missing"); diff --git a/package-lock.json b/package-lock.json index 8bad6c4..2911260 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.5", + "version": "1.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.5", + "version": "1.0.0-beta.1", "license": "CC-BY-NC-ND-4.0", "dependencies": { "discord.js": "^14.6.0", diff --git a/package.json b/package.json index f5b1047..db42af3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eu.astrogd.white-leopard", - "version": "1.0.0-alpha.5", + "version": "1.0.0-beta.1", "description": "A Discord bot that checks channel names for blacklisted words and reverts the changes if necessary", "main": "build/index.js", "scripts": { @@ -9,6 +9,7 @@ "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", + "deploy:prod": "rimraf build && npm run build && npm run deploy-commands:prod && docker build -t astrogd/white-leopard:latest . && docker push astrogd/white-leopard:latest", "start": "npm run build && npm run deploy-commands:dev && docker-compose up --no-start && docker compose start database && node --enable-source-maps .", "migration:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d src/data/dataSource.ts -p src/data/migrations/data", "migration:run": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:run -d src/data/dataSource.ts",