Merge pull request #1 from r-Overwatch2/dev

V1.0.0-beta.1
This commit is contained in:
Lukas | AstroGD 2022-11-24 21:29:42 +01:00 committed by GitHub
commit c8ed870fec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 3144 additions and 0 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Build files
build
# Logs
logs
*.log

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:18-alpine AS builder
ENV NODE_ENV=production
# RUN apk add --no-cache python3 make g++
COPY build/package*.json ./
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"]

132
LICENSE.md Normal file
View File

@ -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.

View File

@ -1,2 +1,35 @@
# 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.
### /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
### /info
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 |
| 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
- 🚀 = Required during CI/CD

33
ci/deploy.ts Normal file
View File

@ -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["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");
// 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);
}
})();

33
ci/devDeploy.ts Normal file
View File

@ -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);
}
})();

27
docker-compose.yml Normal file
View File

@ -0,0 +1,27 @@
version: "3.9"
services:
app:
image: astrogd/white-leopard:dev
build: ./
tty: true
stdin_open: true
depends_on:
- database
restart: unless-stopped
environment:
- TOKEN=$TOKEN
- DB_HOST=database
- DB_USERNAME=$DB_USERNAME
- DB_PASSWORD=$DB_PASSWORD
- DB_DATABASE=$DB_DATABASE
database:
image: postgres:latest
restart: unless-stopped
ports:
- 5432:5432
environment:
- POSTGRES_USER=$DB_USERNAME
- POSTGRES_PASSWORD=$DB_PASSWORD
- POSTGRES_DB=$DB_DATABASE
expose:
- 5432

21
index.ts Normal file
View File

@ -0,0 +1,21 @@
import { config } from "dotenv";
import swapConsole from "./src/tools/consoleSwapper";
config();
swapConsole();
import "./src/client/init";
import "./src/commands";
import "./src/events";
import "./src/cli";
import client from "./src/client";
function shutdown() {
console.log("Shutdown request received");
if (client) client.destroy();
process.exit();
}
process.on("SIGINT", shutdown);
process.on("SIGHUP", shutdown);
process.on("SIGTERM", shutdown);

1571
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "eu.astrogd.white-leopard",
"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": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc && shx cp package-lock.json build/package-lock.json",
"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",
"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",
"url": "git+https://github.com/r-Overwatch2/eu.astrogd.white-leopard.git"
},
"author": "AstroGD Lukas Weber <hello@astrogd.eu> (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",
"rimraf": "^3.0.2",
"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",
"moment": "^2.29.4",
"pg": "^8.8.0",
"typeorm": "^0.3.10"
}
}

80
src/cli/global.ts Normal file
View File

@ -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`);
}

150
src/cli/guild.ts Normal file
View File

@ -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]`);
}

68
src/cli/index.ts Normal file
View File

@ -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`);
}

9
src/client/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { Client, GatewayIntentBits } from "discord.js";
const client = new Client({
intents: [
GatewayIntentBits.Guilds
]
});
export default client;

10
src/client/init.ts Normal file
View File

@ -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})`);
});

197
src/commands/blocklist.ts Normal file
View File

@ -0,0 +1,197 @@
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";
import { Color, Emoji } from "../tools/design";
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<void> {
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(`${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",
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(`${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",
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
}

9
src/commands/ci.ts Normal file
View File

@ -0,0 +1,9 @@
import * as notification from "./notification";
import * as blocklist from "./blocklist";
import * as info from "./info";
const array = [notification.builder.toJSON(), blocklist.builder.toJSON(), info.builder.toJSON()];
export {
array
}

43
src/commands/index.ts Normal file
View File

@ -0,0 +1,43 @@
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<string, { builder: SlashCommandBuilder, execute: (interaction: ChatInputCommandInteraction) => Promise<void> }>();
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;
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();
}
});

66
src/commands/info.ts Normal file
View File

@ -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<void> {
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
}

View File

@ -0,0 +1,111 @@
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 { getGuildSetting } from "../tools/data";
import { getGuildChannel } from "../tools/discord";
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(false);
return option;
});
async function resetNotificationChannel(guildSetting: GuildSetting, interaction: ChatInputCommandInteraction): Promise<void> {
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})`
});
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<boolean> | VoiceChannel | ForumChannel | APIInteractionDataResolvedChannel): TextBasedChannel {
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;
}
throw new TypeError("Channel is not a text based channel");
}
export {
builder,
execute
}

33
src/data/dataSource.ts Normal file
View File

@ -0,0 +1,33 @@
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"
});
dataSource.initialize();
export default dataSource;

9
src/data/index.ts Normal file
View File

@ -0,0 +1,9 @@
import dataSource from "./dataSource";
import { Badword, GuildSetting } from "./model";
export {
dataSource as database,
Badword,
GuildSetting
}

View File

@ -0,0 +1,34 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class data1669251793386 implements MigrationInterface {
name = 'data1669251793386'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
DROP TABLE "guild_setting"
`);
await queryRunner.query(`
DROP TABLE "badword"
`);
}
}

View File

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class data1669300160536 implements MigrationInterface {
name = 'data1669300160536'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`
ALTER TABLE "guild_setting" DROP COLUMN "isPremiumUntil"
`);
await queryRunner.query(`
ALTER TABLE "guild_setting"
ADD "isPremiumUntil" date
`);
}
}

13
src/data/model/badword.ts Normal file
View File

@ -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;
}

View File

@ -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("timestamp", { nullable: true, default: null })
isPremiumUntil!: Date | null;
}

7
src/data/model/index.ts Normal file
View File

@ -0,0 +1,7 @@
import { Badword } from "./badword";
import { GuildSetting } from "./guildSetting";
export {
Badword,
GuildSetting
}

View File

@ -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();
});

1
src/events/index.ts Normal file
View File

@ -0,0 +1 @@
import "./channelUpdate";

View File

@ -0,0 +1,63 @@
/* 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/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/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()}`;
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;
}

26
src/tools/data.ts Normal file
View File

@ -0,0 +1,26 @@
import { database, GuildSetting } from "../data";
export async function getGuildSetting(guildID: string): Promise<GuildSetting> {
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 {
if (timestamp === null) return false;
const now = Number(new Date());
const activeUntil = Number(timestamp);
return now < activeUntil;
}

View File

@ -0,0 +1,32 @@
import pack from "../../package.json";
import { EmbedBuilder } from "discord.js";
import client from "../client";
import { Color, Emoji } from "./design";
// 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(Color.ANONYMOUS_GRAY);
return embed;
}
export function getSuccessEmbed(): EmbedBuilder {
const embed = getDefaultEmbed();
embed.setTitle(`${Emoji.CHAT_APPROVE} Success`);
embed.setColor(Color.SUCCESSFUL_GREEN);
return embed;
}
export function getFailedEmbed(): EmbedBuilder {
const embed = getDefaultEmbed();
embed.setTitle(`${Emoji.CHAT_DENY} Failed`);
embed.setColor(Color.STOPSIGN_RED);
return embed;
}

30
src/tools/design.ts Normal file
View File

@ -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
}

10
src/tools/discord.ts Normal file
View File

@ -0,0 +1,10 @@
import { GuildBasedChannel } from "discord.js";
import client from "../client";
export async function getGuildChannel(guildID: string, channelID: string): Promise<GuildBasedChannel | null> {
const guild = await client.guilds.fetch(guildID);
if (!guild) return null;
const channel = await guild.channels.fetch(channelID);
return channel;
}

107
tsconfig.json Normal file
View File

@ -0,0 +1,107 @@
{
"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 '<reference>'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. */
},
"include": [
"index.ts",
"src"
]
}