diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml
new file mode 100644
index 0000000..de695ae
--- /dev/null
+++ b/.github/workflows/pipeline.yml
@@ -0,0 +1,26 @@
+name: Build Sokora
+
+on:
+ push:
+ branches:
+ - dev
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '20.x'
+
+ - name: Install packages and build
+ run: npm install && npx tsc -b ./
+
diff --git a/.gitignore b/.gitignore
index 3ec544c..00ad2e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
node_modules/
-.env
\ No newline at end of file
+.env
+.DS_Store
+data.db
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..9d5a120
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "useTabs": false,
+ "arrowParens": "avoid",
+ "trailingComma": "none",
+ "printWidth": 100
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4d62514..a6f3644 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,12 +1,12 @@
-# Nebula Contributing Guide
+# Sokora Contributing Guide
## Prerequisites
- Basic knowledge of [TypeScript](https://typescriptlang.org/) and [discord.js](https://discord.js.org/).
-- [Bun](https://bun.sh) and [MySQL](https://mysql.com/) installed.
+- [Bun](https://bun.sh) installed.
-## Get started with developing
+## Get started with contributing
### Getting the code
-- Make a fork of this repository.
+- Make a fork of this repository.
- Clone your fork.
### Creating your bot
@@ -14,18 +14,11 @@
- Invite your bot to your server.
- Reset and then copy your bot's token.
-### Setting up your database
-We use MySQL for the database. You need to set one up to be able to run the bot.
-
-- Create a database file called `json.db`
-- It is recommended to create a specific user for Nebula only. See [the docs](https://dev.mysql.com/doc/refman/8.0/en/creating-accounts.html) for more information
-
### Setting up .env
-- Copy the `example.env` file and replace the content with your own credentials inside a file called `.env`.
+- Run `bun run setup` and our cli tool will install dependencies and write .env for you
### Running
-- Run `bun i` to install all the modules needed by the bot.
-- Run `bun start`
+- Run `bun start`.
Be sure to open a pull request when you're ready to push your changes. Be descriptive of the changes you've made.
diff --git a/README.md b/README.md
index 3596e2f..6685757 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,17 @@
-
+
-
+
# About
-Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.
+Sokora is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.
-**Please note that Nebula is currently in an alpha preview state and only usable within Discord with limited functionality.**
+**Please note that Sokora is currently unstable and is only usable within Discord.**
# Contributing
While we're developing the multiplatform version of the bot, you can still [help us](CONTRIBUTING.MD) if you find any bugs.
-
-Only bug fixes are accepted, **no new features!**
diff --git a/bun.lockb b/bun.lockb
index 8c93d62..e1a9baa 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/cli/setup.ts b/cli/setup.ts
new file mode 100644
index 0000000..52c22d3
--- /dev/null
+++ b/cli/setup.ts
@@ -0,0 +1,50 @@
+import * as fs from "fs";
+import { createInterface } from "readline";
+
+const readHiddenInput = async (prompt: string): Promise => {
+ console.log(prompt);
+
+ return new Promise(resolve => {
+ const rl = createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+
+ const stdin = process.stdin;
+ if (stdin.isTTY) stdin.setRawMode(true);
+
+ stdin.on("data", data => {
+ if (data.toString() === "\n" || data.toString() === "\r") {
+ if (stdin.isTTY) stdin.setRawMode(false);
+ process.stdout.moveCursor(0, -1);
+ process.stdout.clearLine(1);
+ rl.close();
+ }
+ });
+
+ rl.question("", input => {
+ resolve(input);
+ });
+ });
+};
+
+const replaceTokenInEnv = (token: string) => {
+ try {
+ const envContent = fs.readFileSync(".env", "utf-8");
+ const updatedContent = envContent.replace("YOURTOKEN", token);
+ fs.writeFileSync(".env", updatedContent, "utf-8");
+ console.log("You're good to go, happy coding!");
+ } catch (error) {
+ console.error("Error updating .env file:", error);
+ }
+};
+
+const main = async () => {
+ fs.copyFileSync("example.env", ".env");
+ const token = await readHiddenInput(
+ "Paste your token below: (you can get one from https://discord.com/developers/applications)"
+ );
+ replaceTokenInEnv(token);
+};
+
+main().catch(console.error);
diff --git a/example.env b/example.env
index 7f366d8..8973729 100644
--- a/example.env
+++ b/example.env
@@ -1,7 +1 @@
-TOKEN="YOURTOKEN" # The bot token found in the Discord Developer portal
-
-MYSQL_HOST="localhost" # Name of the host
-MYSQL_PORT="1234"
-MYSQL_USERNAME="username" # Username USED BY YOUR DATABASE
-MYSQL_DATABASE="nebula_stable"
-MYSQL_PASSWORD="Password123"
+TOKEN="YOURTOKEN"
diff --git a/package.json b/package.json
index d4bc0a5..91dd4ed 100644
--- a/package.json
+++ b/package.json
@@ -1,27 +1,26 @@
{
- "name": "nebula",
- "description": "Welcome to Nebula, the multipurpose, multiplatform bot.",
+ "name": "sokora",
+ "description": "Welcome to Sokora, a multipurpose Discord bot that lets you manage your servers easily.",
"contributors": [
- "The Nebula team",
+ "The Sokora team",
"The GitHub contributors"
],
"version": "0.1.0",
"main": "./src/index.ts",
"type": "module",
"scripts": {
+ "setup": "bun i && bun run cli/setup.ts",
"start": "bun ./src/index.ts"
},
"dependencies": {
- "discord.js": "^14.14.1",
+ "discord.js": "^14.16.3",
"ms": "^2.1.3",
- "mysql2": "^3.6.3",
"node-vibrant": "^3.2.1-alpha.1",
- "quick.db": "^9.1.7",
- "sharp": "v0.33.0-alpha.11"
+ "sharp": "^0.33.5"
},
"devDependencies": {
- "bun-types": "0.8.1",
- "typescript": "^5.2.2"
- },
- "trustedDependencies": ["sharp"]
+ "@types/ms": "^0.7.34",
+ "bun-types": "^1.1.33",
+ "typescript": "^5.6.3"
+ }
}
diff --git a/src/bot.ts b/src/bot.ts
index 7d08495..bd8e19b 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -1,10 +1,11 @@
-import { Client, ActivityType } from "discord.js";
-import Commands from "./handlers/commands.js";
-import Events from "./handlers/events.js";
+import { ActivityType, Client } from "discord.js";
+import { Commands } from "./handlers/commands";
+import { Events } from "./handlers/events";
+import { rescheduleUnbans } from "./utils/unbanScheduler";
const client = new Client({
presence: {
- activities: [{ name: "with /settings!", type: ActivityType.Playing }]
+ activities: [{ name: "your feedback!", type: ActivityType.Listening }]
},
intents: [
"Guilds",
@@ -12,15 +13,16 @@ const client = new Client({
"GuildMessages",
"GuildEmojisAndStickers",
"GuildPresences",
+ "GuildBans",
"MessageContent"
]
});
client.on("ready", async () => {
- new Events(client);
- console.log("Starting all commands.");
+ await new Events(client).loadEvents();
await new Commands(client).registerCommands();
console.log("ちーっす!");
+ rescheduleUnbans(client);
});
client.login(process.env.TOKEN);
diff --git a/src/commands/About.ts b/src/commands/About.ts
new file mode 100644
index 0000000..81adb7a
--- /dev/null
+++ b/src/commands/About.ts
@@ -0,0 +1,78 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ EmbedBuilder,
+ SlashCommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { imageColor } from "../utils/imageColor";
+import { randomise } from "../utils/randomise";
+
+export default class About {
+ data: SlashCommandBuilder;
+ constructor() {
+ this.data = new SlashCommandBuilder()
+ .setName("about")
+ .setDescription("Shows information about Sokora.");
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const client = interaction.client;
+ const user = client.user;
+ const guilds = client.guilds.cache;
+ const members = guilds.map(guild => guild.memberCount).reduce((a, b) => a + b);
+ const shards = client.shard?.count;
+ const avatar = user.displayAvatarURL();
+ let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"];
+ if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"];
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: "• About Sokora", iconURL: avatar })
+ .setDescription(
+ "Sokora is a multipurpose Discord bot that lets you manage your servers easily."
+ )
+ .setFields(
+ {
+ name: "📃 • General",
+ value: [
+ "Version **0.1**, *Kaishi*",
+ `**${members}** members • **${guilds.size}** guild${guilds.size == 1 ? "" : "s"} ${
+ !shards ? "" : `• **${shards}** shard${shards == 1 ? "" : "s"}`
+ }`
+ ].join("\n")
+ },
+ {
+ name: "🌌 • Entities involved",
+ value: [
+ "**Founder**: Goos",
+ "**Translator Lead**: ThatBOI",
+ "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI",
+ "**Designers**: ArtyH, ZakaHaceCosas, Pjanda",
+ "**Translators**: Dimkauzh, flojo, Golem64, GraczNet, Nikkerudon, ZakaHaceCosas, SaFire, TrulyBlue",
+ "**Testers**: Blaze, fishy, Trynera",
+ "And **YOU**, for using Sokora."
+ ].join("\n")
+ },
+ {
+ name: "🔗 • Links",
+ value:
+ "[GitHub](https://www.github.com/NebulaTheBot) • [YouTube](https://www.youtube.com/@NebulaTheBot) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Guilded](https://guilded.gg/Nebula) • [Revolt](https://rvlt.gg/28TS9aXy)"
+ }
+ )
+ .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` })
+ .setThumbnail(avatar)
+ .setColor(user.hexAccentColor ?? (await imageColor(undefined, avatar)) ?? genColor(270));
+
+ const row = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setLabel("• Donate")
+ .setURL("https://paypal.me/SokoraTheBot")
+ .setEmoji("⭐")
+ .setStyle(ButtonStyle.Link)
+ );
+
+ await interaction.reply({ embeds: [embed], components: [row] });
+ }
+}
diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts
new file mode 100644
index 0000000..fa778a5
--- /dev/null
+++ b/src/commands/Leaderboard.ts
@@ -0,0 +1,111 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ EmbedBuilder,
+ SlashCommandBuilder,
+ type ChatInputCommandInteraction,
+ type SlashCommandOptionsOnlyBuilder
+} from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { getGuildLeaderboard } from "../utils/database/leveling";
+import { errorEmbed } from "../utils/embeds/errorEmbed";
+
+export default class Leaderboard {
+ data: SlashCommandOptionsOnlyBuilder;
+ constructor() {
+ this.data = new SlashCommandBuilder()
+ .setName("leaderboard")
+ .setDescription("Displays the guild leaderboard.")
+ .addNumberOption(option => option.setName("page").setDescription("Page number to display."));
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guildID = interaction.guild?.id;
+ if (!guildID) return errorEmbed(interaction, "This command can only be used in a server.");
+
+ const leaderboardData = getGuildLeaderboard(guildID);
+ if (!leaderboardData.length)
+ return errorEmbed(
+ interaction,
+ "No data found.",
+ "There is no leveling data for this server yet."
+ );
+
+ leaderboardData.sort((a, b) => {
+ if (b.level != a.level) return b.level - a.level;
+ else return b.xp - a.xp;
+ });
+
+ const totalPages = Math.ceil(leaderboardData.length / 5);
+ let page = interaction.options.getNumber("page") || 1;
+ page = Math.max(1, Math.min(page, totalPages));
+ const generateEmbed = async () => {
+ const start = (page - 1) * 5;
+ const end = start + 5;
+ const pageData = leaderboardData.slice(start, end);
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: "Leaderboard" })
+ .setColor(genColor(200))
+ .setFooter({ text: `Page ${page}/${totalPages}` });
+
+ for (let i = 0; i < pageData.length; i++) {
+ const userData = pageData[i];
+ const user = await interaction.client.users.fetch(userData.user);
+ embed.addFields({
+ name: `#${start + i + 1} • ${user.tag}`,
+ value: `Level **${Math.floor(userData.level)}** • **${Math.floor(userData.xp)}** XP`
+ });
+ }
+
+ return embed;
+ };
+
+ const row = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("left")
+ .setEmoji("1298708251256291379")
+ .setStyle(ButtonStyle.Primary),
+ new ButtonBuilder()
+ .setCustomId("right")
+ .setEmoji("1298708281493160029")
+ .setStyle(ButtonStyle.Primary)
+ );
+
+ const reply = await interaction.reply({
+ embeds: [await generateEmbed()],
+ components: totalPages > 1 ? [row] : [],
+ fetchReply: true
+ });
+
+ if (totalPages > 1) {
+ const collector = reply.createMessageComponentCollector({
+ time: 30000
+ });
+
+ collector.on("collect", async (i: ButtonInteraction) => {
+ if (i.message.id != (await reply.fetch()).id)
+ return await errorEmbed(
+ i,
+ "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that."
+ );
+
+ if (i.user.id != interaction.user.id)
+ return errorEmbed(i, "You are not the person who executed this command.");
+
+ collector.resetTimer({ time: 30000 });
+ if (i.customId == "left") page = page > 1 ? page - 1 : totalPages;
+ else page = page < totalPages ? page + 1 : 1;
+
+ await i.update({
+ embeds: [await generateEmbed()],
+ components: [row]
+ });
+ });
+
+ collector.on("end", async () => await interaction.editReply({ components: [] }));
+ }
+ }
+}
diff --git a/src/commands/Server.ts b/src/commands/Server.ts
new file mode 100644
index 0000000..9c5ac8f
--- /dev/null
+++ b/src/commands/Server.ts
@@ -0,0 +1,16 @@
+import { SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js";
+import { serverEmbed } from "../utils/embeds/serverEmbed";
+
+export default class Server {
+ data: SlashCommandBuilder;
+ constructor() {
+ this.data = new SlashCommandBuilder()
+ .setName("server")
+ .setDescription("Shows this server's info.");
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const embed = await serverEmbed({ guild: interaction.guild!, roles: true });
+ await interaction.reply({ embeds: [embed] });
+ }
+}
diff --git a/src/commands/Serverboard.ts b/src/commands/Serverboard.ts
new file mode 100644
index 0000000..cef27aa
--- /dev/null
+++ b/src/commands/Serverboard.ts
@@ -0,0 +1,90 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ SlashCommandBuilder,
+ type ChatInputCommandInteraction,
+ type SlashCommandOptionsOnlyBuilder
+} from "discord.js";
+import { listPublicServers } from "../utils/database/settings";
+import { errorEmbed } from "../utils/embeds/errorEmbed";
+import { serverEmbed } from "../utils/embeds/serverEmbed";
+
+export default class Serverboard {
+ data: SlashCommandOptionsOnlyBuilder;
+ constructor() {
+ this.data = new SlashCommandBuilder()
+ .setName("serverboard")
+ .setDescription("Shows the servers that have Sokora.")
+ .addNumberOption(number =>
+ number.setName("page").setDescription("The page you want to see.")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guildList = (
+ await Promise.all(listPublicServers().map(id => interaction.client.guilds.fetch(id)))
+ ).sort((a, b) => b.memberCount - a.memberCount);
+
+ const pages = guildList.length;
+ if (!pages)
+ return await errorEmbed(
+ interaction,
+ "No public server found.",
+ "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings serverboard `shown: True` to make your server publicly visible."
+ );
+
+ const argPage = interaction.options.getNumber("page") as number;
+ let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0;
+
+ async function getEmbed() {
+ return await serverEmbed({ guild: guildList[page], page: page + 1, pages });
+ }
+
+ const row = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("left")
+ .setEmoji("1298708251256291379")
+ .setStyle(ButtonStyle.Primary),
+ new ButtonBuilder()
+ .setCustomId("right")
+ .setEmoji("1298708281493160029")
+ .setStyle(ButtonStyle.Primary)
+ );
+
+ const reply = await interaction.reply({
+ embeds: [await getEmbed()],
+ components: pages != 1 ? [row] : []
+ });
+ if (pages == 1) return;
+
+ const collector = reply.createMessageComponentCollector({ time: 30000 });
+ collector.on("collect", async (i: ButtonInteraction) => {
+ if (i.message.id != (await reply.fetch()).id)
+ return await errorEmbed(
+ i,
+ "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that."
+ );
+
+ if (i.user.id != interaction.user.id)
+ return await errorEmbed(i, "You aren't the person who executed this command.");
+
+ collector.resetTimer({ time: 30000 });
+ switch (i.customId) {
+ case "left":
+ page--;
+ if (page < 0) page = pages - 1;
+ break;
+ case "right":
+ page++;
+ if (page >= pages) page = 0;
+ break;
+ }
+
+ await i.update({ embeds: [await getEmbed()], components: [row] });
+ });
+
+ collector.on("end", async () => await interaction.editReply({ components: [] }));
+ }
+}
diff --git a/src/commands/Settings.ts b/src/commands/Settings.ts
new file mode 100644
index 0000000..472faf7
--- /dev/null
+++ b/src/commands/Settings.ts
@@ -0,0 +1,126 @@
+import {
+ AutocompleteInteraction,
+ EmbedBuilder,
+ InteractionType,
+ PermissionsBitField,
+ SlashCommandBuilder,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../utils/colorGen";
+import {
+ getSetting,
+ setSetting,
+ settingsDefinition,
+ settingsKeys
+} from "../utils/database/settings";
+import { errorEmbed } from "../utils/embeds/errorEmbed";
+
+export default class Settings {
+ data: SlashCommandBuilder;
+ constructor() {
+ this.data = new SlashCommandBuilder()
+ .setName("settings")
+ .setDescription("Configure Sokora to your liking.")
+ .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator);
+
+ settingsKeys.forEach(key => {
+ const subcommand = new SlashCommandSubcommandBuilder()
+ .setName(key)
+ .setDescription(settingsDefinition[key].description);
+
+ Object.keys(settingsDefinition[key].settings).forEach(sub => {
+ switch (settingsDefinition[key].settings[sub]["type"] as string) {
+ case "BOOL":
+ subcommand.addBooleanOption(option =>
+ option
+ .setName(sub)
+ .setDescription(settingsDefinition[key].settings[sub]["desc"])
+ .setRequired(false)
+ );
+ break;
+ case "INTEGER":
+ subcommand.addIntegerOption(option =>
+ option
+ .setName(sub)
+ .setDescription(settingsDefinition[key].settings[sub]["desc"])
+ .setRequired(false)
+ );
+ break;
+ case "USER":
+ subcommand.addUserOption(option =>
+ option
+ .setName(sub)
+ .setDescription(settingsDefinition[key].settings[sub]["desc"])
+ .setRequired(false)
+ );
+ break;
+ default: // Also includes "TEXT"
+ subcommand.addStringOption(option =>
+ option
+ .setName(sub)
+ .setDescription(settingsDefinition[key].settings[sub]["desc"])
+ .setRequired(false)
+ );
+ break;
+ }
+ });
+ this.data.addSubcommand(subcommand);
+ });
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ ?.get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.Administrator)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Administrator** permission."
+ );
+
+ const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition;
+ const values = interaction.options.data[0].options!;
+ if (!values.length) {
+ const embed = new EmbedBuilder().setTitle(`Settings for ${key}`).setColor(genColor(100));
+ const description: string[] = [];
+
+ Object.keys(settingsDefinition[key]).forEach(name => {
+ description.push(`${name}: ${getSetting(guild.id, key, name)?.toString() || "Not set"}`);
+ embed.setDescription(description.join("\n"));
+ });
+
+ return await interaction.reply({ embeds: [embed] });
+ }
+
+ const embed = new EmbedBuilder().setTitle(`Parameters changed`).setColor(genColor(100));
+ values.forEach(option => {
+ setSetting(guild.id, key, option.name, option.value as string);
+ embed.addFields({
+ name: option.name,
+ value: option.value?.toString() || "Not set"
+ });
+ });
+
+ await interaction.reply({ embeds: [embed] });
+ }
+
+ async autocomplete(interaction: AutocompleteInteraction) {
+ if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return;
+ switch (Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0]) {
+ case "BOOL":
+ await interaction.respond(
+ ["true", "false"].map(choice => ({
+ name: choice,
+ value: choice
+ }))
+ );
+ break;
+ default:
+ await interaction.respond([]);
+ }
+ }
+}
diff --git a/src/commands/User.ts b/src/commands/User.ts
new file mode 100644
index 0000000..05c0c47
--- /dev/null
+++ b/src/commands/User.ts
@@ -0,0 +1,156 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ EmbedBuilder,
+ SlashCommandBuilder,
+ type ChatInputCommandInteraction,
+ type SlashCommandOptionsOnlyBuilder
+} from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { getLevel } from "../utils/database/leveling";
+import { getSetting } from "../utils/database/settings";
+import { errorEmbed } from "../utils/embeds/errorEmbed";
+import { imageColor } from "../utils/imageColor";
+
+export default class User {
+ data: SlashCommandOptionsOnlyBuilder;
+ constructor() {
+ this.data = new SlashCommandBuilder()
+ .setName("user")
+ .setDescription("Shows your (or another user's) info.")
+ .addUserOption(user => user.setName("user").setDescription("Select the user."));
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ const user = interaction.options.getUser("user") ?? interaction.user;
+ const target = guild.members.cache.get(user.id);
+ const avatar = target?.displayAvatarURL() ?? user.displayAvatarURL();
+ const embedColor =
+ (await target?.user.fetch())?.hexAccentColor ??
+ (await imageColor(undefined, avatar)) ??
+ genColor(200);
+
+ let embed = new EmbedBuilder()
+ .setAuthor({
+ name: `${avatar ? "• " : ""}${target?.nickname ?? user.displayName}`,
+ iconURL: avatar
+ })
+ .setFields({
+ name: `<:discord:1266797021126459423> • Discord info`,
+ value: [
+ `Username is **${user.username}**`,
+ `Display name is ${
+ user.displayName == user.username ? "*not there*" : `**${user.displayName}**`
+ }`,
+ `Created on ****`
+ ].join("\n")
+ })
+ .setFooter({ text: `User ID: ${user.id}` })
+ .setThumbnail(avatar)
+ .setColor(embedColor);
+
+ await interaction.reply({ embeds: [embed] });
+
+ if (!target) return;
+ let serverInfo = [`Joined on ****`];
+ const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!;
+ const memberRoles = [...guildRoles].sort(
+ (role1, role2) => role2[1].position - role1[1].position
+ );
+ memberRoles.pop();
+ const rolesLength = memberRoles.length;
+
+ if (target.premiumSinceTimestamp)
+ serverInfo.push(`Boosting since **${target.premiumSinceTimestamp}**`);
+
+ if (memberRoles.length)
+ serverInfo.push(
+ `**${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1}** ${
+ memberRoles.length == 1 ? "role" : "roles"
+ } • ${memberRoles
+ .slice(0, 5)
+ .map(role => `<@&${role[1].id}>`)
+ .join(", ")}${rolesLength > 3 ? ` and **${rolesLength - 3}** more` : ""}`
+ );
+
+ embed.addFields({
+ name: "📒 • Server info",
+ value: serverInfo.join("\n")
+ });
+
+ const enabled = getSetting(`${guild.id}`, "leveling", "enabled");
+ const row = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("general")
+ .setLabel("• General")
+ .setEmoji("📃")
+ .setStyle(ButtonStyle.Primary),
+ new ButtonBuilder()
+ .setCustomId("level")
+ .setLabel("• Level")
+ .setEmoji("⚡")
+ .setStyle(ButtonStyle.Primary)
+ );
+ row.components[0].setDisabled(true);
+ const reply = await interaction.editReply({
+ embeds: [embed],
+ components: !user.bot ? (enabled ? [row] : []) : []
+ });
+
+ if (!enabled && user.bot) return;
+ const difficulty = getSetting(guild.id, "leveling", "difficulty") as number;
+ const [level, xp] = getLevel(guild.id, target.id)!;
+ const nextLevelXp = Math.floor(
+ 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2
+ )?.toLocaleString("en-US");
+
+ const collector = reply.createMessageComponentCollector({ time: 30000 });
+ collector.on("collect", async (i: ButtonInteraction) => {
+ if (i.message.id != (await reply.fetch()).id)
+ return await errorEmbed(
+ i,
+ "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that."
+ );
+
+ if (i.user.id != interaction.user.id)
+ return await errorEmbed(i, "You aren't the person who executed this command.");
+
+ collector.resetTimer({ time: 30000 });
+ i.customId == "general"
+ ? row.components[0].setDisabled(true)
+ : row.components[1].setDisabled(true);
+
+ const levelEmbed = new EmbedBuilder()
+ .setAuthor({
+ name: `• ${target.nickname ?? user.displayName}`,
+ iconURL: target.displayAvatarURL()
+ })
+ .setFields({
+ name: `⚡ • Level ${level}`,
+ value: [
+ `**${xp.toLocaleString("en-US")}/${nextLevelXp}** XP`,
+ `The next level is **${level + 1}**`
+ ].join("\n")
+ })
+ .setFooter({ text: `User ID: ${target.id}` })
+ .setThumbnail(target.displayAvatarURL())
+ .setColor(embedColor);
+
+ switch (i.customId) {
+ case "general":
+ row.components[1].setDisabled(false);
+ await i.update({ embeds: [embed], components: [row] });
+ break;
+ case "level":
+ row.components[0].setDisabled(false);
+ await i.update({ embeds: [levelEmbed], components: [row] });
+ break;
+ }
+ });
+
+ collector.on("end", async () => await interaction.editReply({ components: [] }));
+ }
+}
diff --git a/src/commands/info/news.ts b/src/commands/info/news.ts
deleted file mode 100644
index 603a771..0000000
--- a/src/commands/info/news.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder,
- ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import { getNewsTable } from "../../utils/database.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class News {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("news")
- .setDescription("The news of the current server you're in.")
- .addNumberOption(option => option
- .setName("page")
- .setDescription("The page of the news you want to see")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsletterTable = await getNewsTable(db);
-
- const guild = interaction.guild;
- let page = interaction.options.getNumber("page") ?? 1;
-
- const news = await newsletterTable?.get(`${guild.id}.news`).then(
- (news: any) => news as any[] ?? []
- ).catch(() => []);
- if (!news) return await interaction.followUp({
- embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")]
- });
-
- const newsSorted = (Object.values(news).map((newsItem: any, i) => {
- return {
- id: Object.keys(news)[i],
- ...newsItem
- }
- }) as any[])?.sort((a, b) => b.createdAt - a.createdAt);
- if (!newsSorted) return await interaction.followUp({
- embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")]
- });
-
- if (newsSorted.length == 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")]
- });
- }
-
- if (page > newsSorted.length) page = newsSorted.length;
- if (page < 1) page = 1;
-
- let currentNews = newsSorted[page - 1];
- let newsEmbed = new EmbedBuilder()
- .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null })
- .setTitle(currentNews.title)
- .setDescription(currentNews.body)
- .setImage(currentNews.imageURL || null)
- .setTimestamp(parseInt(currentNews.updatedAt))
- .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` })
- .setColor(genColor(200));
-
- const row = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId("left")
- .setEmoji("1137330341472915526")
- .setStyle(ButtonStyle.Primary),
- new ButtonBuilder()
- .setCustomId("right")
- .setEmoji("1137330125004869702")
- .setStyle(ButtonStyle.Primary)
- );
-
- await interaction.followUp({ embeds: [newsEmbed], components: [row] });
-
- const buttonCollector = interaction.channel.createMessageComponentCollector({
- filter: i => i.user.id === interaction.user.id,
- time: 60000
- });
-
- buttonCollector.on("collect", async i => {
- if (!i.isButton()) return;
- const id = i.customId;
-
- if (id == "left") {
- page--;
- if (page < 1) page = newsSorted.length;
- } else if (id == "right") {
- page++;
- if (page > newsSorted.length) page = 1;
- }
-
- currentNews = newsSorted[page - 1];
- newsEmbed = new EmbedBuilder()
- .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null })
- .setTitle(currentNews.title)
- .setDescription(currentNews.body)
- .setImage(currentNews.imageURL || null)
- .setTimestamp(parseInt(currentNews.updatedAt))
- .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` })
- .setColor(genColor(200));
-
- await interaction.editReply({ embeds: [newsEmbed], components: [row] });
- await i.deferUpdate();
- });
- }
-}
diff --git a/src/commands/info/server.ts b/src/commands/info/server.ts
deleted file mode 100644
index eaa0482..0000000
--- a/src/commands/info/server.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js";
-import serverEmbed from "../../utils/embeds/serverEmbed.js";
-
-export default class Server {
- data: SlashCommandSubcommandBuilder;
- constructor() {
- this.data = new SlashCommandSubcommandBuilder()
- .setName("server")
- .setDescription("Shows this server's info.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const embed = await serverEmbed({ guild: interaction.guild, roles: true });
-
- await interaction.followUp({ embeds: [embed] });
- }
-}
diff --git a/src/commands/info/subscribe.ts b/src/commands/info/subscribe.ts
deleted file mode 100644
index 0239fd2..0000000
--- a/src/commands/info/subscribe.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction, DMChannel
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import { getNewsTable } from "../../utils/database.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-type SubscriptionType = string[];
-
-export default class Subscribe {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("subscribe")
- .setDescription("Subscribe to the news of the current server you're in.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsTable = await getNewsTable(db);;
-
- const guild = interaction.guild;
- const user = interaction.user;
-
- let subscriptions = await newsTable?.get(`${guild.id}.subscriptions`).then(
- (subscriptions: any) => subscriptions as SubscriptionType ?? [] as SubscriptionType
- )
- .catch(() => []) as SubscriptionType;
- if (!subscriptions) subscriptions = [];
-
- const hasSub = subscriptions?.includes(user.id);
-
- const dmChannel = (await interaction.user.createDM().catch(() => null)) as DMChannel | null;
- if (!dmChannel) return await interaction.followUp({
- embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")]
- });
- const sendDms = await dmChannel?.send("You have updated the subscription status of \`" + guild.name + "\`.").catch(() => { });
- if (!sendDms) {
- await newsTable.pull(`${guild.id}.subscriptions`, user.id);
- return await interaction.followUp({
- embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")]
- });
- }
-
- await newsTable[!hasSub ? "push" : "pull"](`${guild.id}.subscriptions`, user.id);
-
- const subscriptionEmbed = new EmbedBuilder()
- .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} ${guild.name}`)
- .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of ${guild.name}.`)
- .setColor(genColor(100));
-
- await interaction.followUp({ embeds: [subscriptionEmbed] });
- }
-}
diff --git a/src/commands/info/user.ts b/src/commands/info/user.ts
deleted file mode 100644
index fadb42c..0000000
--- a/src/commands/info/user.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import {
- ColorResolvable,
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- type ChatInputCommandInteraction,
-} from "discord.js";
-import { genColor, genRGBColor } from "../../utils/colorGen.js";
-import Vibrant from "node-vibrant";
-import sharp from "sharp";
-
-export default class User {
- data: SlashCommandSubcommandBuilder;
- constructor() {
- this.data = new SlashCommandSubcommandBuilder()
- .setName("user")
- .setDescription("Shows your (or another user's) info.")
- .addUserOption((option) => option.setName("user").setDescription("Select the user."));
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const user = interaction.options.getUser("user");
- const member = interaction.member;
- const guild = interaction.guild;
-
- const id = user ? user.id : member.user.id;
- const selectedMember = guild.members.cache.filter((member) => member.user.id === id).map((user) => user)[0];
- const avatarURL = selectedMember.displayAvatarURL();
- const selectedUser = selectedMember.user;
-
- // Sorting the roles
- const guildRoles = guild.roles.cache.filter((role) => selectedMember.roles.cache.has(role.id));
- const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position);
- memberRoles.pop();
- const rolesOrRole = memberRoles.length === 1 ? "role" : "roles";
-
- let embed = new EmbedBuilder()
- .setAuthor({
- name: `• ${selectedMember.nickname == null ? selectedUser.username : selectedMember.nickname}${selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}`
- }`,
- iconURL: avatarURL,
- })
- .setFields(
- {
- name: selectedUser.bot === false ? "👤 • User info" : "🤖 • Bot info",
- value: [
- `**Username**: ${selectedUser.username}`,
- `**Display name**: ${selectedUser.displayName === selectedUser.username ? "*None*" : selectedUser.displayName
- }`,
- `**Created on** `,
- ].join("\n"),
- },
- {
- name: "👥 • Member info",
- value: `**Joined on** `,
- }
- )
- .setFooter({ text: `User ID: ${selectedMember.id}` })
- .setThumbnail(avatarURL)
- .setColor(genColor(200));
-
- try {
- const imageBuffer = await (await fetch(avatarURL)).arrayBuffer();
- const image = sharp(imageBuffer).toFormat("jpg");
- const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant;
- embed.setColor(genRGBColor(r, g, b) as ColorResolvable);
- } catch { }
-
- if (memberRoles.length != 0) {
- embed.addFields({
- name: `🎭 • ${guildRoles.filter((role) => selectedMember.roles.cache.has(role.id)).size - 1} ${rolesOrRole}`,
- value: `${memberRoles
- .slice(0, 5)
- .map((role) => `<@&${role[1].id}>`)
- .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}`,
- });
- }
-
- await interaction.followUp({ embeds: [embed] });
- }
-}
diff --git a/src/commands/leveling/leaderboard.ts b/src/commands/leveling/leaderboard.ts
deleted file mode 100644
index 233a4d2..0000000
--- a/src/commands/leveling/leaderboard.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import { getLevelingTable, getSettingsTable } from "../../utils/database.js";
-import { BASE_EXP_FOR_NEW_LEVEL, DIFFICULTY_MULTIPLIER } from "../../events/leveling.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class Leaderboard {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("leaderboard")
- .setDescription("Shows the server's leaderboard in levels.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const levelingTable = await getLevelingTable(db);
-
- // Under maintenance
- return await interaction.followUp({
- embeds: [errorEmbed("This command is under maintenance.")]
- });
-
- const guild = interaction.guild;
- const levelEnabled = await settingsTable?.get(`${guild.id}.leveling.enabled`).catch(() => { });
- const levels = await levelingTable?.get(`${guild.id}`).catch(() => { });
- const levelKeys = Object.keys(levels)
- const convertLevelsAndExpToExp = (levels: any) => {
- const exp = Object.keys(levels).map(level => levels[level].exp);
- return exp.reduce((a, b) => a + b);
- };
-
- if (!levelEnabled)
- return await interaction.followUp({ embeds: [errorEmbed("Leveling is disabled for this server.")] });
-
- const levelUpEmbed = new EmbedBuilder()
- .setTitle("⚡ • Top 10 active members")
- .setDescription(
- levelKeys.slice(0, 10).map(level =>
- `#${Object.keys(levels).indexOf(level) + 1} • <@${level}>\n**Level ${levels[level].levels}** - Next Level: ${levels[level].levels + 1}\n**Exp**: ${levels[level].exp}/${Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levels[level].levels + 1))} until level up`
- ).join("\n\n")
- )
- .setColor(genColor(200))
- .setTimestamp();
-
- await interaction.followUp({ embeds: [levelUpEmbed] });
- }
-}
diff --git a/src/commands/leveling/level.ts b/src/commands/leveling/level.ts
deleted file mode 100644
index ddd5c21..0000000
--- a/src/commands/leveling/level.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import {
- ColorResolvable,
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor, genRGBColor } from "../../utils/colorGen.js";
-import Vibrant from "node-vibrant";
-import sharp from "sharp";
-import { getLevelingTable, getSettingsTable } from "../../utils/database.js";
-import { BASE_EXP_FOR_NEW_LEVEL, DIFFICULTY_MULTIPLIER } from "../../events/leveling.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { Reward } from "../settings/leveling/rewards.js";
-
-export default class Level {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("level")
- .setDescription("Shows your (or another user's) level.")
- .addUserOption((option) => option.setName("user").setDescription("Select the user."));
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const levelingTable = await getLevelingTable(db);
-
- const user = interaction.options.getUser("user");
- const member = interaction.member;
- const guild = interaction.guild;
-
- const id = user ? user.id : member.user.id;
- const selectedMember = guild.members.cache.filter((member) => member.user.id === id).map((user) => user)[0];
- const avatarURL = selectedMember.displayAvatarURL();
-
- const levelEnabled = await settingsTable?.get(`${guild.id}.leveling.enabled`).then(
- (data) => {
- if (!data) return false;
- return Boolean(data);
- }
- ).catch(() => false);
- if (!levelEnabled) {
- return await interaction.followUp({
- embeds: [errorEmbed("Leveling is disabled for this server.")]
- });
- }
-
- const { exp, levels } = await levelingTable?.get(`${guild.id}.${selectedMember.id}`).then(
- (data) => {
- if (!data) return { exp: 0, level: 0 };
- return { exp: Number(data.exp), levels: Number(data.levels) };
- }
- ).catch(() => {
- return { exp: 0, levels: 0 };
- });
- const formattedExp = exp?.toLocaleString("en-US");
-
- if (!exp && !levels) {
- await levelingTable.set(`${guild.id}.${selectedMember.id}`, {
- levels: 0,
- exp: 0
- });
- }
-
- let rewards = [];
- let nextReward = null;
- const levelRewards = await settingsTable?.get(`${interaction.guild.id}.leveling.rewards`).then(
- (data) => {
- if (!data) return [] as Reward[] ?? [] as Reward[];
- return data as Reward[] ?? [] as Reward[];
- }
- ).catch(() => [] as Reward[]);
-
- for (const { roleId, level } of levelRewards) {
- const role = await interaction.guild.roles.fetch(roleId).catch(() => { });
- const reward = { roleId, level };
-
- if (levels < level) {
- if (nextReward) break;
- nextReward = reward;
- break;
- }
- rewards.push(role);
- }
-
- const expUntilLevelup = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * ((levels ?? 0) + 1));
- const formattedExpUntilLevelup = expUntilLevelup?.toLocaleString("en-US");
- const levelUpEmbed = new EmbedBuilder()
- .setFields([
- {
- name: `⚡ • Level ${levels ?? 0}`,
- value: [
- `**Exp**: ${formattedExp ?? 0}/${formattedExpUntilLevelup} until level up`,
- `**Next Level**: ${(levels ?? 0) + 1}`
- ].join("\n")
- },
- {
- name: `🎁 • ${rewards.length} Rewards`,
- value: [
- `${rewards.length > 0 ? rewards.map(reward => `<@&${reward.id}>`).join(" ") : "No rewards unlocked"
- }`,
- nextReward ? `**Upcoming reward**: <@&${nextReward.roleId}>` : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!"
- ].join("\n")
- }
- ])
- .setAuthor({
- name: `• ${selectedMember.user.username}`,
- iconURL: avatarURL
- })
- .setThumbnail(avatarURL)
- .setColor(genColor(200))
- .setTimestamp();
-
- try {
- const imageBuffer = await (await fetch(avatarURL)).arrayBuffer();
- const image = sharp(imageBuffer).toFormat("jpg");
- const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant;
- levelUpEmbed.setColor(genRGBColor(r, g, b) as ColorResolvable);
- } catch { }
-
- await interaction.followUp({ embeds: [levelUpEmbed] });
- }
-}
diff --git a/src/commands/leveling/rewards.ts b/src/commands/leveling/rewards.ts
deleted file mode 100644
index 513f539..0000000
--- a/src/commands/leveling/rewards.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction,
- Role,
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export type Reward = {
- roleId: string,
- level: number,
-}
-
-export default class Rewards {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("rewards")
- .setDescription("Sets/gets reward roles for each level -> No options = shows.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- // List the rewards
- const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a.level - b.level)) as Reward[];
- if (rewards.length == 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("There are no rewards set for this server.")]
- });
- }
-
- const rewardsEmbed = new EmbedBuilder()
- .setTitle("🎁 • Rewards")
- .setDescription("Here are the rewards in this server.")
- .setColor(genColor(100));
-
- for (const { roleId, level } of rewards) {
- if (!roleId) continue;
- rewardsEmbed.addFields([{
- name: `Level ${level}`,
- value: `<@&${roleId}>`,
- }]);
- }
-
- return await interaction.followUp({
- embeds: [rewardsEmbed]
- });
- }
-
- async getRewards(guildId: string): Promise {
- const settingsTable = await getSettingsTable(this.db);
- const rewards = new Promise((resolve, reject) => {
- settingsTable?.get(`${guildId}.leveling.rewards`).then(rewards => {
- if (!rewards) return resolve([]);
- resolve(rewards);
- }).catch(() => {
- resolve([]);
- });
- });
- return rewards as Promise;
- }
-}
diff --git a/src/commands/moderation/Ban.ts b/src/commands/moderation/Ban.ts
new file mode 100644
index 0000000..0dbc4de
--- /dev/null
+++ b/src/commands/moderation/Ban.ts
@@ -0,0 +1,73 @@
+import {
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import ms from "ms";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed";
+import { scheduleUnban } from "../../utils/unbanScheduler";
+
+export default class Ban {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("ban")
+ .setDescription("Bans a user.")
+ .addUserOption(user =>
+ user.setName("user").setDescription("The user that you want to ban.").setRequired(true)
+ )
+ .addStringOption(string => string.setName("reason").setDescription("The reason for the ban."))
+ .addStringOption(string =>
+ string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const user = interaction.options.getUser("user")!;
+ const guild = interaction.guild!;
+ const duration = interaction.options.getString("duration");
+ const reason = interaction.options.getString("reason");
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.BanMembers,
+ { interaction, user, action: "Ban" },
+ { allErrors: true, botError: true, ownerError: true },
+ "Ban Members"
+ )
+ )
+ return;
+
+ if ((await guild.bans.fetch()).get(user.id))
+ return await errorEmbed(
+ interaction,
+ `You can't ban ${user.displayName}.`,
+ "This user is already banned."
+ );
+
+ let expiresAt: number | undefined;
+ if (duration) {
+ const durationMs = ms(duration);
+ if (!durationMs || durationMs <= 0)
+ return await errorEmbed(
+ interaction,
+ `You can't ban ${user.displayName} temporarily.`,
+ "The duration is invalid."
+ );
+
+ expiresAt = Date.now() + durationMs;
+ scheduleUnban(interaction.client, guild.id, user.id, interaction.member!.user.id, durationMs);
+ }
+
+ try {
+ await guild.members.ban(user.id, { reason: reason ?? undefined });
+ } catch (err) {
+ console.error("Failed to ban user:", err);
+ }
+
+ await modEmbed(
+ { interaction, user, action: "Banned", duration, dm: true, dbAction: "BAN", expiresAt },
+ reason
+ );
+ }
+}
diff --git a/src/commands/moderation/Cases.ts b/src/commands/moderation/Cases.ts
new file mode 100644
index 0000000..021d805
--- /dev/null
+++ b/src/commands/moderation/Cases.ts
@@ -0,0 +1,92 @@
+import {
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { getModeration, listUserModeration } from "../../utils/database/moderation";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { randomise } from "../../utils/randomise";
+
+export default class Cases {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("cases")
+ .setDescription("Moderation cases in a server.")
+ .addUserOption(user =>
+ user.setName("user").setDescription("The user that you want to see.").setRequired(true)
+ )
+ .addStringOption(string =>
+ string.setName("id").setDescription("The ID of a specific moderation case you want to see.")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const actionsEmojis: { [key: string]: string } = {
+ WARN: "⚠️",
+ MUTE: "🔇",
+ KICK: "📤",
+ BAN: "🔨"
+ };
+
+ const nothingMsg = [
+ "Nothing to see here...",
+ "Ayay, no cases on this horizon cap'n!",
+ "Clean as a whistle!",
+ "0+0=?"
+ ];
+
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ModerateMembers)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Moderate Members** permission."
+ );
+
+ const user = interaction.options.getUser("user")!;
+ // const warns = listUserModeration(guild.id, user.id, "WARN");
+ // const mutes = listUserModeration(guild.id, user.id, "MUTE");
+ // const kicks = listUserModeration(guild.id, user.id, "KICK");
+ // const bans = listUserModeration(guild.id, user.id, "BAN");
+ let actionID = interaction.options.getString("id");
+ if (actionID && actionID?.startsWith("#")) actionID = actionID.slice(1);
+ const actions = actionID
+ ? getModeration(guild.id, user.id, actionID)
+ : listUserModeration(guild.id, user.id);
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• Cases of ${user.displayName}`, iconURL: user.displayAvatarURL() })
+ .setFields(
+ actions.length > 0
+ ? actions.map(action => {
+ const actionValues = [
+ `**Moderator**: <@${action.moderator}>`,
+ action.reason ? `**Reason**: ${action.reason}` : "*No reason provided*",
+ `-# **Time of action**: `
+ ];
+
+ return {
+ name: `${actionsEmojis[action.type]} • ${action.type} #${action.id}`, // Include durations ? needs to add a db column
+ value: actionValues.join("\n")
+ };
+ })
+ : [
+ {
+ name: `💨 • ${randomise(nothingMsg)}`,
+ value: "*No actions have been taken on this user*"
+ }
+ ]
+ )
+ .setFooter({ text: `User ID: ${user.id}` })
+ .setColor(genColor(200));
+
+ await interaction.reply({ embeds: [embed] });
+ }
+}
diff --git a/src/commands/moderation/Clear.ts b/src/commands/moderation/Clear.ts
new file mode 100644
index 0000000..2332999
--- /dev/null
+++ b/src/commands/moderation/Clear.ts
@@ -0,0 +1,85 @@
+import {
+ ChannelType,
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { logChannel } from "../../utils/logChannel";
+
+export default class Clear {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("clear")
+ .setDescription("Clears messages.")
+ .addNumberOption(number =>
+ number
+ .setName("amount")
+ .setDescription("The amount of messages that you want to clear (maximum is 100).")
+ .setRequired(true)
+ )
+ .addChannelOption(channel =>
+ channel
+ .setName("channel")
+ .setDescription("The channel that has the messages that you want to clear.")
+ .addChannelTypes(
+ ChannelType.GuildText,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread,
+ ChannelType.GuildVoice
+ )
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageMessages)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Messages** permission."
+ );
+
+ const amount = interaction.options.getNumber("amount")!;
+ if (amount > 100)
+ return await errorEmbed(interaction, "You can only clear up to 100 messages at a time.");
+
+ if (amount < 1) return await errorEmbed(interaction, "You must clear at least 1 message.");
+
+ const channelOption = interaction.options.getChannel("channel")!;
+ const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!;
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `Cleared ${amount} message${amount == 1 ? "" : "s"}.` })
+ .setDescription(
+ [
+ `**Moderator**: ${interaction.user.displayName}`,
+ `**Channel**: ${channelOption ?? `<#${channel.id}>`}`
+ ].join("\n")
+ )
+ .setColor(genColor(100));
+
+ if (
+ channel.type == ChannelType.GuildText &&
+ ChannelType.PublicThread &&
+ ChannelType.PrivateThread &&
+ ChannelType.GuildVoice
+ )
+ try {
+ channel == interaction.channel
+ ? await channel.bulkDelete(amount + 1, true)
+ : await channel.bulkDelete(amount, true);
+ } catch (error) {
+ console.error(error);
+ }
+
+ await logChannel(guild, embed);
+ await interaction.reply({ embeds: [embed] });
+ }
+}
diff --git a/src/commands/moderation/Delwarn.ts b/src/commands/moderation/Delwarn.ts
new file mode 100644
index 0000000..88af050
--- /dev/null
+++ b/src/commands/moderation/Delwarn.ts
@@ -0,0 +1,70 @@
+import {
+ DMChannel,
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { listUserModeration, removeModeration } from "../../utils/database/moderation";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { errorCheck } from "../../utils/embeds/modEmbed";
+import { logChannel } from "../../utils/logChannel";
+
+export default class Delwarn {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("delwarn")
+ .setDescription("Removes a warning from a user.")
+ .addUserOption(user =>
+ user
+ .setName("user")
+ .setDescription("The user that you want to free from the warning.")
+ .setRequired(true)
+ )
+ .addNumberOption(number =>
+ number.setName("id").setDescription("The id of the warn.").setRequired(true)
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const user = interaction.options.getUser("user")!;
+ const guild = interaction.guild!;
+ const name = user.displayName;
+ const id = interaction.options.getNumber("id");
+ const warns = listUserModeration(guild.id, user.id, "WARN");
+ const newWarns = warns.filter(warn => warn.id != `${id}`);
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.ModerateMembers,
+ { interaction, user, action: "Remove a warn" },
+ { allErrors: true, botError: false },
+ "Moderate Members"
+ )
+ )
+ return;
+
+ if (newWarns.length == warns.length)
+ return await errorEmbed(interaction, `There is no warn with the id of ${id}.`);
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• Removed a warning from ${name}`, iconURL: user.displayAvatarURL() })
+ .setDescription(`**Moderator**: ${interaction.user.displayName}`)
+ .setFooter({ text: `User ID: ${user.id}` })
+ .setColor(genColor(100));
+
+ await logChannel(guild, embed);
+ try {
+ removeModeration(guild.id, `${id}`);
+ } catch (error) {
+ console.error(error);
+ }
+
+ await interaction.reply({ embeds: [embed] });
+ const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;
+ if (!dmChannel) return;
+ if (user.bot) return;
+ await dmChannel.send({ embeds: [embed.setTitle("Your warning has been removed.")] });
+ }
+}
diff --git a/src/commands/moderation/Kick.ts b/src/commands/moderation/Kick.ts
new file mode 100644
index 0000000..c67bae8
--- /dev/null
+++ b/src/commands/moderation/Kick.ts
@@ -0,0 +1,50 @@
+import {
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed";
+
+export default class Kick {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("kick")
+ .setDescription("Kicks a user.")
+ .addUserOption(user =>
+ user.setName("user").setDescription("The user that you want to kick.").setRequired(true)
+ )
+ .addStringOption(string =>
+ string.setName("reason").setDescription("The reason for the kick.")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const user = interaction.options.getUser("user")!;
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.KickMembers,
+ { interaction, user, action: "Kick" },
+ { allErrors: true, botError: true, ownerError: true, outsideError: true },
+ "Kick Members"
+ )
+ )
+ return;
+
+ if (!interaction.guild?.members.cache.get(user.id))
+ return await errorEmbed(
+ interaction,
+ `You can't kick ${user.displayName}.`,
+ "This user is not in the server."
+ );
+
+ const reason = interaction.options.getString("reason");
+ await interaction.guild?.members.cache
+ .get(user.id)
+ ?.kick(reason ?? undefined)
+ .catch(error => console.error(error));
+
+ await modEmbed({ interaction, user, action: "Kicked", dm: true, dbAction: "KICK" }, reason);
+ }
+}
diff --git a/src/commands/moderation/Lock.ts b/src/commands/moderation/Lock.ts
new file mode 100644
index 0000000..7197865
--- /dev/null
+++ b/src/commands/moderation/Lock.ts
@@ -0,0 +1,81 @@
+import {
+ ChannelType,
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { logChannel } from "../../utils/logChannel";
+
+export default class Lock {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("lock")
+ .setDescription("Locks a channel.")
+ .addChannelOption(channel =>
+ channel
+ .setName("channel")
+ .setDescription("The channel that you want to lock.")
+ .addChannelTypes(
+ ChannelType.GuildText,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread,
+ ChannelType.GuildVoice
+ )
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageRoles)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Roles** permission."
+ );
+
+ const channelOption = interaction.options.getChannel("channel")!;
+ const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!;
+ if (!channel.permissionsFor(guild.id)?.has("SendMessages"))
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "The channel is already locked."
+ );
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `Locked a channel.` })
+ .setDescription(
+ [
+ `**Moderator**: ${interaction.user.displayName}`,
+ `**Channel**: ${channelOption ?? `<#${channel.id}>`}`
+ ].join("\n")
+ )
+ .setColor(genColor(100));
+
+ if (
+ channel.type == ChannelType.GuildText &&
+ ChannelType.PublicThread &&
+ ChannelType.PrivateThread &&
+ ChannelType.GuildVoice
+ )
+ channel.permissionOverwrites
+ .create(guild.id, {
+ SendMessages: false,
+ SendMessagesInThreads: false,
+ CreatePublicThreads: false,
+ CreatePrivateThreads: false
+ })
+ .catch(error => console.error(error));
+
+ await logChannel(guild, embed);
+ await interaction.reply({ embeds: [embed] });
+ }
+}
diff --git a/src/commands/moderation/Mute.ts b/src/commands/moderation/Mute.ts
new file mode 100644
index 0000000..b32ad71
--- /dev/null
+++ b/src/commands/moderation/Mute.ts
@@ -0,0 +1,66 @@
+import {
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import ms from "ms";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed";
+
+export default class Mute {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("mute")
+ .setDescription("Mutes a user.")
+ .addUserOption(user =>
+ user.setName("user").setDescription("The user that you want to mute.").setRequired(true)
+ )
+ .addStringOption(string =>
+ string
+ .setName("duration")
+ .setDescription("The duration of the mute (e.g 30m, 1d, 2h).")
+ .setRequired(true)
+ )
+ .addStringOption(string =>
+ string.setName("reason").setDescription("The reason for the mute.")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const user = interaction.options.getUser("user")!;
+ const duration = interaction.options.getString("duration")!;
+ const reason = interaction.options.getString("reason");
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.ModerateMembers,
+ { interaction, user, action: "Mute" },
+ { allErrors: true, botError: true, ownerError: true },
+ "Moderate Members"
+ )
+ )
+ return;
+
+ if (!ms(duration) || ms(duration) > ms("28d") || ms(duration) <= 0)
+ return await errorEmbed(
+ interaction,
+ `You can't mute ${user.displayName}.`,
+ "The duration is invalid or is above the 28 day limit."
+ );
+
+ const time = new Date(
+ Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString())
+ ).toISOString();
+
+ await interaction.guild?.members.cache
+ .get(user.id)
+ ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined })
+ .catch(error => console.error(error));
+
+ await modEmbed(
+ { interaction, user, action: "Muted", duration, dm: true, dbAction: "MUTE" },
+ reason,
+ true
+ );
+ }
+}
diff --git a/src/commands/moderation/Slowdown.ts b/src/commands/moderation/Slowdown.ts
new file mode 100644
index 0000000..3451595
--- /dev/null
+++ b/src/commands/moderation/Slowdown.ts
@@ -0,0 +1,89 @@
+import {
+ ChannelType,
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import ms from "ms";
+import { genColor } from "../../utils/colorGen";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { logChannel } from "../../utils/logChannel";
+
+export default class Slowdown {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("slowdown")
+ .setDescription("Slows a channel down.")
+ .addStringOption(string =>
+ string
+ .setName("time")
+ .setDescription(
+ "Time to slow the channel down to (e.g 30m, 1d, 2h). 0 to remove slowdown."
+ )
+ .setRequired(true)
+ )
+ .addStringOption(string =>
+ string.setName("reason").setDescription("The reason for the slowdown.")
+ )
+ .addChannelOption(channel =>
+ channel
+ .setName("channel")
+ .setDescription("The channel that you want to slowdown.")
+ .addChannelTypes(
+ ChannelType.GuildText,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread,
+ ChannelType.GuildVoice
+ )
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageChannels)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Channels** permission."
+ );
+
+ const time = interaction.options.getString("time")!;
+ const reason = interaction.options.getString("reason");
+ const channelOption = interaction.options.getChannel("channel")!;
+ const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!;
+ let title = `Set a slowdown of \`${channelOption ?? `${channel.name}`}\` to ${ms(ms(time), {
+ long: true
+ })}.`;
+ if (!ms(time)) title = `Removed the slowdown from \`${channelOption ?? `${channel.name}`}\`.`;
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: title })
+ .setDescription(
+ [
+ `**Moderator**: ${interaction.user.displayName}`,
+ reason ? `**Reason**: ${reason}` : "*No reason provided*",
+ `**Channel**: ${channelOption ?? `<#${channel.id}>**`}`
+ ].join("\n")
+ )
+ .setColor(genColor(100));
+
+ if (
+ channel.type == ChannelType.GuildText &&
+ ChannelType.PublicThread &&
+ ChannelType.PrivateThread &&
+ ChannelType.GuildVoice
+ )
+ await channel
+ .setRateLimitPerUser(ms(time) / 1000, interaction.options.getString("reason")!)
+ .catch(error => console.error(error));
+
+ await logChannel(guild, embed);
+ await interaction.reply({ embeds: [embed] });
+ }
+}
diff --git a/src/commands/moderation/Unban.ts b/src/commands/moderation/Unban.ts
new file mode 100644
index 0000000..a8d5d8e
--- /dev/null
+++ b/src/commands/moderation/Unban.ts
@@ -0,0 +1,52 @@
+import {
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed";
+
+export default class Unban {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("unban")
+ .setDescription("Unbans a user.")
+ .addStringOption(string =>
+ string
+ .setName("id")
+ .setDescription("The ID of the user that you want to unban.")
+ .setRequired(true)
+ )
+ .addStringOption(string =>
+ string.setName("reason").setDescription("The reason for the unban.")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const id = interaction.options.getString("id")!;
+ const reason = interaction.options.getString("reason")!;
+ const guild = interaction.guild!;
+ const target = (await guild.bans.fetch()).get(id)?.user!;
+
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.BanMembers,
+ { interaction, user: target, action: "Unban" },
+ { allErrors: false, botError: true, ownerError: true },
+ "Ban Members"
+ )
+ )
+ return;
+
+ if (!target)
+ return await errorEmbed(
+ interaction,
+ "You can't unban this user.",
+ "The user was never banned."
+ );
+
+ await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error));
+ await modEmbed({ interaction, user: target, action: "Unbanned" }, reason);
+ }
+}
diff --git a/src/commands/moderation/Unlock.ts b/src/commands/moderation/Unlock.ts
new file mode 100644
index 0000000..edba7ee
--- /dev/null
+++ b/src/commands/moderation/Unlock.ts
@@ -0,0 +1,81 @@
+import {
+ ChannelType,
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { logChannel } from "../../utils/logChannel";
+
+export default class Unlock {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("unlock")
+ .setDescription("Unlocks a channel.")
+ .addChannelOption(channel =>
+ channel
+ .setName("channel")
+ .setDescription("The channel that you want to unlock.")
+ .addChannelTypes(
+ ChannelType.GuildText,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread,
+ ChannelType.GuildVoice
+ )
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageRoles)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Roles** permission."
+ );
+
+ const channelOption = interaction.options.getChannel("channel")!;
+ const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!;
+ if (channel.permissionsFor(guild.id)?.has("SendMessages"))
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "The channel is not locked."
+ );
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `Unlocked a channel.` })
+ .setDescription(
+ [
+ `**Moderator**: ${interaction.user.displayName}`,
+ `**Channel**: ${channelOption ?? `<#${channel.id}>`}`
+ ].join("\n")
+ )
+ .setColor(genColor(100));
+
+ if (
+ channel.type == ChannelType.GuildText &&
+ ChannelType.PublicThread &&
+ ChannelType.PrivateThread &&
+ ChannelType.GuildVoice
+ )
+ channel.permissionOverwrites
+ .create(guild.id, {
+ SendMessages: null,
+ SendMessagesInThreads: null,
+ CreatePublicThreads: null,
+ CreatePrivateThreads: null
+ })
+ .catch(error => console.error(error));
+
+ await logChannel(guild, embed);
+ await interaction.reply({ embeds: [embed] });
+ }
+}
diff --git a/src/commands/moderation/Unmute.ts b/src/commands/moderation/Unmute.ts
new file mode 100644
index 0000000..622e95b
--- /dev/null
+++ b/src/commands/moderation/Unmute.ts
@@ -0,0 +1,43 @@
+import {
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed";
+
+export default class Unmute {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("unmute")
+ .setDescription("Unmutes a user.")
+ .addUserOption(user =>
+ user.setName("user").setDescription("The user that you want to unmute.").setRequired(true)
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const user = interaction.options.getUser("user")!;
+ const target = interaction.guild?.members.cache.get(user.id)!;
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.ModerateMembers,
+ { interaction, user, action: "Unmute" },
+ { allErrors: false, botError: true },
+ "Moderate Members"
+ )
+ )
+ return;
+
+ if (!target.communicationDisabledUntil)
+ return await errorEmbed(
+ interaction,
+ "You can't unmute this user.",
+ "The user was never muted."
+ );
+
+ await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error));
+ await modEmbed({ interaction, user, action: "Unmuted" });
+ }
+}
diff --git a/src/commands/moderation/Warn.ts b/src/commands/moderation/Warn.ts
new file mode 100644
index 0000000..a458908
--- /dev/null
+++ b/src/commands/moderation/Warn.ts
@@ -0,0 +1,49 @@
+import {
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed";
+
+export default class Warn {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("warn")
+ .setDescription("Warns a user.")
+ .addUserOption(user =>
+ user.setName("user").setDescription("The user that you want to warn.").setRequired(true)
+ )
+ .addStringOption(string =>
+ string.setName("reason").setDescription("The reason for the warn.")
+ )
+ .addBooleanOption(bool =>
+ bool
+ .setName("show_moderator")
+ .setDescription(
+ "Inform the warned user of the moderator that took the action. Defaults to false."
+ )
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const user = interaction.options.getUser("user")!;
+ const reason = interaction.options.getString("reason");
+ const showModerator = interaction.options.getBoolean("show_moderator") ?? false;
+ if (
+ await errorCheck(
+ PermissionsBitField.Flags.ModerateMembers,
+ { interaction, user, action: "Warn" },
+ { allErrors: true, botError: false, ownerError: true, outsideError: true },
+ "Moderate Members"
+ )
+ )
+ return;
+
+ await modEmbed(
+ { interaction, user, action: "Warned", dm: true, dbAction: "WARN" },
+ reason,
+ showModerator
+ );
+ }
+}
diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts
deleted file mode 100644
index 6f6115b..0000000
--- a/src/commands/moderation/ban.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, TextChannel, DMChannel
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getSettingsTable } from "../../utils/database.js";
-
-export default class Ban {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("ban")
- .setDescription("Bans a user.")
- .addUserOption(option => option
- .setName("user")
- .setDescription("The user that you want to ban.")
- .setRequired(true)
- )
- .addStringOption(option => option
- .setName("reason")
- .setDescription("The reason for the ban.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const user = interaction.options.getUser("user");
- const reason = interaction.options.getString("reason");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
- const selectedMember = members.get(user.id);
- const name = selectedMember.nickname ?? user.username;
-
- const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;;
- const banEmbed = new EmbedBuilder()
- .setTitle(`✅ • Banned ${user.username}`)
- .setDescription([
- `**Moderator**: <@${interaction.user.id}>`,
- `**Reason**: ${reason ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
-
- const embedDM = new EmbedBuilder()
- .setTitle(`🔨 • You were banned`)
- .setDescription([
- `**Moderator**: ${interaction.user.username}`,
- `**Reason**: ${reason ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(0));
-
- if (!member.permissions.has(PermissionsBitField.Flags.BanMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] });
- if (selectedMember === member)
- return await interaction.followUp({ embeds: [errorEmbed("You can't ban yourself")] });
- if (selectedMember.user.id === interaction.client.user.id)
- return await interaction.followUp({ embeds: [errorEmbed("You can't ban Nebula.")] });
- if (!selectedMember.manageable)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than Nebula.`)] });
- if (member.roles.highest.position < selectedMember.roles.highest.position)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than you.`)] });
-
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel ?? null
- ).catch(() => null);
- if (logChannel) {
- const channel = (await interaction.guild.channels.cache.get(logChannel).fetch()) as TextChannel;
- await channel.send({ embeds: [banEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await selectedMember.ban({ reason: reason ?? undefined });
- await interaction.followUp({ embeds: [banEmbed] });
- }
-}
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
deleted file mode 100644
index 2a433cd..0000000
--- a/src/commands/moderation/kick.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, TextChannel, DMChannel,
- Channel, ChannelType
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getSettingsTable } from "../../utils/database.js";
-
-export default class Kick {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("kick")
- .setDescription("Kicks a user.")
- .addUserOption(option => option
- .setName("user")
- .setDescription("The user that you want to kick.")
- .setRequired(true)
- )
- .addStringOption(option => option
- .setName("reason")
- .setDescription("The reason for the kick.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const user = interaction.options.getUser("user");
- const reason = interaction.options.getString("reason");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
- const selectedMember = members.get(user.id);
- const name = selectedMember.nickname ?? user.username;
-
- const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;;
- const kickEmbed = new EmbedBuilder()
- .setTitle(`✅ • Kicked <@${user.id}>`)
- .setDescription([
- `**Moderator**: <@${interaction.user.id}>`,
- `**Reason**: ${reason ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
-
- const embedDM = new EmbedBuilder()
- .setTitle(`👢 • You were kicked`)
- .setDescription([
- `**Moderator**: ${interaction.user.username}`,
- `**Reason**: ${reason ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(0));
-
- if (!member.permissions.has(PermissionsBitField.Flags.KickMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Kick Members** permission to execute this command.")] });
- if (selectedMember === member)
- return await interaction.followUp({ embeds: [errorEmbed("You can't kick yourself.")] });
- if (selectedMember.user.id === interaction.client.user.id)
- return await interaction.followUp({ embeds: [errorEmbed("You can't kick Nebula.")] });
- if (!selectedMember.manageable)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than Nebula.`)] });
- if (member.roles.highest.position < selectedMember.roles.highest.position)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than you.`)] });
-
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel ?? null
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel | null) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [kickEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await selectedMember.kick(reason ?? undefined);
- await interaction.followUp({ embeds: [kickEmbed] });
- }
-}
diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts
deleted file mode 100644
index 9ac2d47..0000000
--- a/src/commands/moderation/mute.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, TextChannel, DMChannel,
- Channel, ChannelType
-} from "discord.js";
-import ms from "ms";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Mute {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("mute")
- .setDescription("Mutes a user.")
- .addUserOption(user => user
- .setName("user")
- .setDescription("The user that you want to mute.")
- .setRequired(true)
- )
- .addStringOption(string => string
- .setName("duration")
- .setDescription("The duration of the mute (e.g 30m, 1d, 2h)")
- .setRequired(true)
- )
- .addStringOption(string => string
- .setName("reason")
- .setDescription("The reason for the mute.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const user = interaction.options.getUser("user");
- const duration = interaction.options.getString("duration");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
- const selectedMember = members.get(user.id);
- const name = selectedMember.nickname ?? user.username;
-
- const ISOduration = new Date(ms(duration)).toISOString();
- const time = new Date(Date.parse(new Date().toISOString()) + Date.parse(ISOduration)).toISOString();
-
- const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;
- const muteEmbed = new EmbedBuilder()
- .setTitle(`✅ • Muted ${user.username}`)
- .setDescription([
- `**Moderator**: <@${member.id}>`,
- `**Duration**: ${ms(ms(duration), { long: true })}`,
- `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}`
- ].join("\n"))
- .setFooter({ text: `User ID: ${user.id}` })
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setColor(genColor(100));
- const embedDM = new EmbedBuilder()
- .setTitle(`🤐 • You were muted`)
- .setDescription([
- `**Moderator**: ${member.user.username}`,
- `**Duration**: ${ms(ms(duration), { long: true })}`,
- `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(0));
-
- if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] });
- if (selectedMember === member)
- return await interaction.followUp({ embeds: [errorEmbed("You can't mute yourself.")] });
- if (selectedMember.user.bot)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't mute ${name}, because it's a bot.`)] });
- if (!selectedMember.manageable)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than Nebula.`)] });
- if (member.roles.highest.position < selectedMember.roles.highest.position)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than you.`)] });
- if (!ms(duration) || ms(duration) > ms("28d"))
- return await interaction.followUp({ embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] });
-
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [muteEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await selectedMember.edit({ communicationDisabledUntil: time });
- await interaction.followUp({ embeds: [muteEmbed] });
- }
-}
diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts
deleted file mode 100644
index 94f2e49..0000000
--- a/src/commands/moderation/purge.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- ChannelType, type ChatInputCommandInteraction, TextChannel,
- Channel
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Purge {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("purge")
- .setDescription("Purges messages.")
- .addNumberOption((number) =>
- number.setName("amount").setDescription("The amount of messages that you want to purge.").setRequired(true)
- )
- .addChannelOption((channel) =>
- channel
- .setName("channel")
- .setDescription("The channel that you want to purge (if not the current channel)")
- .addChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const amount = interaction.options.getNumber("amount");
- if (amount > 100) {
- return await interaction.followUp({
- embeds: [errorEmbed("You can only purge up to 100 messages at a time.")],
- });
- }
- if (amount < 1) {
- return await interaction.followUp({
- embeds: [errorEmbed("You must purge at least 1 message.")],
- });
- }
- const channelOption = interaction.options.getChannel("channel");
- const member = interaction.guild.members.cache.get(interaction.member.user.id);
- const channel = interaction.guild.channels.cache.get(interaction.channel.id ?? channelOption.id);
-
- const purgeEmbed = new EmbedBuilder()
- .setTitle(`✅ • Purged ${amount} messages.`)
- .setDescription([
- `**Moderator**: <@${member.id}>`,
- `**Channel**: ${channelOption ?? `<#${channel.id}>`}`,
- ].join("\n"))
- .setColor(genColor(100));
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")],
- });
- }
- if (channel.type === ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread) {
- channel == interaction.channel
- ? await channel.bulkDelete(amount + 1, true)
- : await channel.bulkDelete(amount, true);
- }
-
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [purgeEmbed] });
- }
-
- await interaction.followUp({ embeds: [purgeEmbed] });
- }
-}
diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts
deleted file mode 100644
index 4633dd3..0000000
--- a/src/commands/moderation/unban.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- PermissionsBitField, EmbedBuilder, SlashCommandSubcommandBuilder,
- type ChatInputCommandInteraction, TextChannel, DMChannel,
- Channel, ChannelType
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Unban {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("unban")
- .setDescription("Unbans a user.")
- .addStringOption(string => string
- .setName("user")
- .setDescription("The ID of the user that you want to unban.")
- .setRequired(true)
- )
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const userID = interaction.options.getString("user");
- const member = interaction.guild.members.cache.get(interaction.member.user.id);
- const bannedMembers = interaction.guild.bans.cache;
- const bannedMemberArray = bannedMembers.map(user => user.user);
- const selectedBannedMember = bannedMemberArray.filter(user => user.id === userID)[0];
-
- const dmChannel = (await selectedBannedMember.createDM().catch(() => null)) as DMChannel | null;;
- const unbanEmbed = new EmbedBuilder()
- .setTitle(`✅ • Unbanned ${member.user.username}`)
- .setDescription([
- `**Moderator**: <@${member.user.id}>`
- ].join("\n"))
- .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL() })
- .setThumbnail(selectedBannedMember.displayAvatarURL())
- .setFooter({ text: `User ID: ${userID}` })
- .setColor(genColor(100));
- const embedDM = new EmbedBuilder()
- .setTitle(`🤝 • You were unbanned`)
- .setDescription([
- `**Moderator**: ${member.user.username}`
- ].join("\n"))
- .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL() })
- .setThumbnail(selectedBannedMember.displayAvatarURL())
- .setFooter({ text: `User ID: ${userID}` })
- .setColor(genColor(100));
-
- if (!member.permissions.has(PermissionsBitField.Flags.BanMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] });
- if (selectedBannedMember == undefined)
- return await interaction.followUp({ embeds: [errorEmbed("You can't unban this user because they were never banned.")] });
-
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [unbanEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await interaction.guild.members.unban(userID);
- await interaction.followUp({ embeds: [unbanEmbed] });
- }
-}
diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts
deleted file mode 100644
index 72d16ab..0000000
--- a/src/commands/moderation/unmute.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, TextChannel, DMChannel,
- Channel, ChannelType
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Unmute {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("unmute")
- .setDescription("Unmutes a user.")
- .addUserOption(user => user
- .setName("user")
- .setDescription("The user that you want to unmute.")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const user = interaction.options.getUser("user");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
- const selectedMember = members.get(user.id);
- const name = selectedMember.nickname ?? user.username;
-
- const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;;
- let unmuteEmbed = new EmbedBuilder()
- .setTitle(`✅ • Unmuted ${user.username}`)
- .setDescription([
- `**Moderator**: <@${interaction.user.id}>`,
- `**Date**: `
- ].join("\n"))
- .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() })
- .setThumbnail(user.displayAvatarURL())
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
- const embedDM = new EmbedBuilder()
- .setTitle(`🤝 • You were unmuted`)
- .setDescription([
- `**Moderator**: ${interaction.user.username}`,
- `**Date**: `
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
-
- if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] });
-
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [unmuteEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await selectedMember.edit({ communicationDisabledUntil: null });
- await interaction.followUp({ embeds: [unmuteEmbed] });
- }
-}
diff --git a/src/commands/moderation/unwarn.ts b/src/commands/moderation/unwarn.ts
deleted file mode 100644
index 1e69b0f..0000000
--- a/src/commands/moderation/unwarn.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, TextChannel, DMChannel,
- ChannelType, Channel
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getModerationTable, getSettingsTable } from "../../utils/database.js";
-
-export default class Unwarn {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("unwarn")
- .setDescription("Warns a user.")
- .addUserOption(user => user
- .setName("user")
- .setDescription("The user that you want to warn.")
- .setRequired(true)
- )
- .addNumberOption(string => string
- .setName("id")
- .setDescription("The id of the warn.")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const modTable = await getModerationTable(db);
-
- const user = interaction.options.getUser("user");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
- const selectedMember = members.get(user.id);
- const name = selectedMember.nickname ?? user.username;
- const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;;
- const id = interaction.options.getNumber("id", true);
- const warns = await modTable?.get(`${interaction.guild.id}.${user.id}.warns`).then(
- warns => warns as any[] ?? []
- ).catch(() => []);
- const newWarns = warns.filter(warn => warn.id !== id);
- if (newWarns.length === warns.length)
- return await interaction.followUp({ embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] });
-
- const unwarnEmbed = new EmbedBuilder()
- .setTitle(`✅ • Removed warning`)
- .setDescription([
- `**Moderator**: <@${member.id}>`,
- `**Original Reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}`
- ].join("\n"))
- .setFooter({ text: `User ID: ${user.id}` })
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setColor(genColor(100));
- const embedDM = new EmbedBuilder()
- .setTitle(`🤝 • You were unwarned`)
- .setDescription([
- `**Moderator**: ${member.user.username}`,
- `**Original Reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
-
- if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] });
- if (selectedMember === member)
- return await interaction.followUp({ embeds: [errorEmbed("You can't unwarn yourself.")] });
- if (!selectedMember.manageable)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than Nebula.`)] });
- if (member.roles.highest.position < selectedMember.roles.highest.position)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than you.`)] });
-
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [unwarnEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await modTable?.set(`${interaction.guild.id}.${user.id}.warns`, newWarns);
- await interaction.followUp({ embeds: [unwarnEmbed] });
- }
-}
diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts
deleted file mode 100644
index fa3d9cd..0000000
--- a/src/commands/moderation/warn.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, TextChannel, DMChannel,
- Channel, ChannelType
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getModerationTable, getSettingsTable } from "../../utils/database.js";
-
-export default class Warn {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("warn")
- .setDescription("Warns a user.")
- .addUserOption(user => user
- .setName("user")
- .setDescription("The user that you want to warn.")
- .setRequired(true)
- )
- .addStringOption(string => string
- .setName("reason")
- .setDescription("The reason for the warn.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const modTable = await getModerationTable(db);
-
- const user = interaction.options.getUser("user");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
- const selectedMember = members.get(user.id);
- const name = selectedMember.nickname ?? user.username;
- const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;;
-
- const newWarn = {
- id: Date.now(),
- userId: user.id,
- moderator: member.id,
- reason: interaction.options.getString("reason") ?? "No reason provided"
- };
-
- const warnEmbed = new EmbedBuilder()
- .setTitle(`✅ • Warned ${user.username}`)
- .setDescription([
- `**Moderator**: <@${member.id}>`,
- `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
- const embedDM = new EmbedBuilder()
- .setTitle(`😡 • You were warned`)
- .setDescription([
- `**Moderator**: ${member.user.username}`,
- `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}`
- ].join("\n"))
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(0));
-
-
- if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] });
- if (selectedMember === member)
- return await interaction.followUp({ embeds: [errorEmbed("You can't warn yourself.")] });
- if (selectedMember.user.bot)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't warn ${name}, because it's a bot.`)] });
- if (!selectedMember.manageable)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than Nebula.`)] });
- if (member.roles.highest.position < selectedMember.roles.highest.position)
- return await interaction.followUp({ embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`)] });
-
- const settingsTable = await getSettingsTable(db);
- const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then(
- (channel: string | null) => channel
- ).catch(() => null);
- if (logChannel) {
- const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then(
- (channel: Channel) => {
- if (channel.type != ChannelType.GuildText) return null;
- return channel as TextChannel;
- }
- ).catch(() => null);
- if (channel) await channel.send({ embeds: [warnEmbed] });
- }
-
- if (dmChannel) await dmChannel.send({ embeds: [embedDM] });
- await modTable?.push(`${interaction.guild.id}.${user.id}.warns`, newWarn);
- await interaction.followUp({ embeds: [warnEmbed] });
- }
-}
diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts
deleted file mode 100644
index 2a42fc5..0000000
--- a/src/commands/moderation/warns.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getModerationTable } from "../../utils/database.js";
-
-type Warn = {
- id: number;
- moderator: string;
- reason: string;
-}
-
-export default class Warns {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("warns")
- .setDescription("Warns of a user.")
- .addUserOption(user => user
- .setName("user")
- .setDescription("The user that you want to see.")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const modTable = await getModerationTable(db);
-
- const user = interaction.options.getUser("user");
- const members = interaction.guild.members.cache;
- const member = members.get(interaction.member.user.id);
-
- const warns = await modTable.get(`${interaction.guild.id}.${user.id}.warns`).then(
- warns => {
- if (!warns) return [] as Warn[];
- return warns as Warn[] ?? [] as Warn[];
- }
- ).catch(() => [] as Warn[]);
-
- const warnsEmbed = new EmbedBuilder()
- .setTitle(`✅ • Warns of ${user.username}`)
- .setFields(
- warns.length > 0 ? warns.map(warn => {
- return {
- name: `#${warn.id}`,
- value: [
- `**Moderator**: <@${warn.moderator}>`,
- `**Reason**: ${warn.reason}`,
- `**Date**: `,
- ].join("\n"),
- inline: true,
- };
- }) : [{
- name: "No warns",
- value: "This user has no warns."
- }]
- )
- .setThumbnail(user.displayAvatarURL())
- .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() })
- .setFooter({ text: `User ID: ${user.id}` })
- .setColor(genColor(100));
-
- if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers))
- return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] });
-
- await interaction.followUp({ embeds: [warnsEmbed] });
- }
-}
diff --git a/src/commands/nebula/about.ts b/src/commands/nebula/about.ts
deleted file mode 100644
index a43f228..0000000
--- a/src/commands/nebula/about.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import randomise from "../../utils/randomise.js";
-
-export default class About {
- data: SlashCommandSubcommandBuilder;
- constructor() {
- this.data = new SlashCommandSubcommandBuilder()
- .setName("about")
- .setDescription("Shows information about the bot.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const client = interaction.client;
- const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"];
-
- const aboutEmbed = new EmbedBuilder()
- .setAuthor({ name: "• About", iconURL: client.user.displayAvatarURL() })
- .setDescription("Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.")
- .setFields(
- {
- name: "📃 • General",
- value: ["**Version**: v0.1-alpha", `**Guild count**: ${client.guilds.cache.size}`].join("\n"),
- inline: true
- },
- {
- name: "🌌 • Entities involved",
- value: [
- "**Head developer**: Goos",
- "**Developers**: Golem64, Pigpot, ThatBOI",
- "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black",
- "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI",
- "And **YOU**, for using Nebula."
- ].join("\n")
- }
- )
- .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` })
- .setThumbnail(client.user.displayAvatarURL())
- .setColor(genColor(270));
-
- await interaction.followUp({ embeds: [aboutEmbed] });
- }
-}
diff --git a/src/commands/nebula/apply.ts b/src/commands/nebula/apply.ts
deleted file mode 100644
index 6a72777..0000000
--- a/src/commands/nebula/apply.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import randomise from "../../utils/randomise.js";
-
-export default class About {
- data: SlashCommandSubcommandBuilder;
- constructor() {
- this.data = new SlashCommandSubcommandBuilder()
- .setName("apply")
- .setDescription("Apply as a team member in Nebula!");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const client = interaction.client;
- const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞", "❣️", "❤️🔥"];
-
- const applyEmbed = new EmbedBuilder()
- .setAuthor({ name: `• Apply for team!`, iconURL: client.user.displayAvatarURL() })
- .setDescription("We're currently looking for people that could extend our team to help us make Nebula faster and better!")
- .setFields(
- {
- name: "👥 • Who we're looking for",
- value: [
- "- **Developers**: Code and implement our back-end in [TypeScript](https://www.typescriptlang.org/).",
- "- **Front-End Devs**: Make great experiences for our users by making the designs interactive using [Svelte](https://svelte.dev/).",
- "- **UI/UX/Icon designers**: Help us make Nebula not only work great, but also look great."
- ].join("\n")
- },
- {
- name: "❓ • How to apply",
- value: [
- "Join our [team applicants server](https://discord.gg/5hkHwGCbju) to find information on applying, resources + getting interviewed at.",
- `Thank you for your interest at joining Nebula, we appreciate it ${randomise(hearts)}`
- ].join("\n\n")
- }
- )
- .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` })
- .setThumbnail(client.user.displayAvatarURL())
- .setColor(genColor(270));
-
- await interaction.followUp({ embeds: [applyEmbed] });
- }
-}
diff --git a/src/commands/nebula/donate.ts b/src/commands/nebula/donate.ts
deleted file mode 100644
index b86025c..0000000
--- a/src/commands/nebula/donate.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-
-export default class Donate {
- data: SlashCommandSubcommandBuilder;
- constructor() {
- this.data = new SlashCommandSubcommandBuilder()
- .setName("donate")
- .setDescription("Helps us keep up Nebula!");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const donationEmbed = new EmbedBuilder()
- .setTitle("Donate")
- .setDescription([
- "Thanks for being interested into donating for Nebula.",
- "We'll be grateful for any donation and use it towards paying our server costs and software costs.",
- "[Click here](https://nebula.fyreblitz.com/) to donate via PayPal!"
- ].join("\n"))
- .setFields({
- name: "🤔 • Why should I donate?",
- value: [
- "By donating, you help the Nebula team to continue existing and keep production going as well as keeping our servers alive - Nebula is completely free and, as of now, doesn't have any other way to have upkeep or funding, and donating is a great way to help us.",
- "If you donated, you can reach out to us to request a special donators role to flex to your friends.",
- "Additionally, you're helping us keep up the servers and make more amazing features :)."
- ].join("\n\n")
- })
- .setThumbnail(interaction.client.user.avatarURL())
- .setColor(genColor(270));
-
- await interaction.followUp({ embeds: [donationEmbed] });
- }
-}
diff --git a/src/commands/nebula/news.ts b/src/commands/nebula/news.ts
deleted file mode 100644
index a5f3fd8..0000000
--- a/src/commands/nebula/news.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder,
- ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import { getNewsTable } from "../../utils/database.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class News {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("news")
- .setDescription("The news of Nebula.")
- .addNumberOption(option => option
- .setName("page")
- .setDescription("The page of the news you want to see")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsletterTable = await getNewsTable(db);
-
- const nebulaId = "903852579837059113"
- let page = interaction.options.getNumber("page") ?? 1;
-
- const news = await newsletterTable.get(`${nebulaId}.news`).then(
- (news: any) => news as any[] ?? []
- ).catch(() => []);
- const newsSorted = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt);
-
- if (newsSorted.length == 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")]
- });
- }
-
- if (page > newsSorted.length) page = newsSorted.length;
- if (page < 1) page = 1;
-
- let currentNews = newsSorted[page - 1];
- let newsEmbed = new EmbedBuilder()
- .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null })
- .setTitle(currentNews.title)
- .setDescription(currentNews.body)
- .setImage(currentNews.imageURL || null)
- .setTimestamp(parseInt(currentNews.updatedAt))
- .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` })
- .setColor(genColor(270));
-
- const row = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId("left")
- .setEmoji("1137330341472915526")
- .setStyle(ButtonStyle.Primary),
- new ButtonBuilder()
- .setCustomId("right")
- .setEmoji("1137330125004869702")
- .setStyle(ButtonStyle.Primary)
- );
-
- await interaction.followUp({ embeds: [newsEmbed], components: [row] });
-
- const buttonCollector = interaction.channel.createMessageComponentCollector({
- filter: i => i.user.id === interaction.user.id,
- time: 60000
- });
-
- buttonCollector.on("collect", async i => {
- if (!i.isButton()) return;
- const id = i.customId;
-
- if (id == "left") {
- page--;
- if (page < 1) page = newsSorted.length;
- } else if (id == "right") {
- page++;
- if (page > newsSorted.length) page = 1;
- }
-
- currentNews = newsSorted[page - 1];
- newsEmbed = new EmbedBuilder()
- .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null })
- .setTitle(currentNews.title)
- .setDescription(currentNews.body)
- .setImage(currentNews.imageURL || null)
- .setTimestamp(parseInt(currentNews.updatedAt))
- .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` })
- .setColor(genColor(270));
-
- await interaction.editReply({ embeds: [newsEmbed], components: [row] });
- await i.deferUpdate();
- });
- }
-}
diff --git a/src/commands/nebula/serverboard.ts b/src/commands/nebula/serverboard.ts
deleted file mode 100644
index 60f8d42..0000000
--- a/src/commands/nebula/serverboard.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder,
- ButtonStyle, type ChatInputCommandInteraction, ButtonInteraction,
- CacheType, StringSelectMenuInteraction, UserSelectMenuInteraction,
- RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ChannelSelectMenuInteraction
-} from "discord.js";
-import quickSort from "../../utils/quickSort.js";
-import serverEmbed from "../../utils/embeds/serverEmbed.js";
-import database, { getNewsTable, getServerboardTable } from "../../utils/database.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class Serverboard {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("serverboard")
- .setDescription("Shows the servers that have Nebula.")
- .addNumberOption(option => option
- .setName("page")
- .setDescription("The page you want to see.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const serverbTable = await getServerboardTable(db);
-
- // Receiving necessary data
- const guilds = interaction.client.guilds.cache;
- const guildsMapped = {};
-
- // Simplifying the data
- const shownGuilds = (await serverbTable.all().catch(() => [])) as any[];
- for (const guild of guilds.values()) {
- const shownVal = shownGuilds?.find(shown => shown?.id == guild.id)?.value?.shown;
- const isShown = shownVal == null ? null : Boolean(shownVal);
- const isCommunity = guild?.rulesChannelId != null;
-
- if (isShown == false) continue;
- if (isShown == null && !isCommunity) continue;
-
- const members = guild.memberCount + ":" + guild.id;
- guildsMapped[members] = guild;
- }
-
- if (Object.keys(guildsMapped).length == 0)
- return await interaction.followUp({
- embeds: [errorEmbed("There are no servers with Nebula in them that are shown.")]
- });
-
- // Sorting the data
- const sortedGuilds = quickSort(
- [...Object.keys(guildsMapped).map(i => Number(i.split(":")[0]))],
- [[...Object.values(guildsMapped)]],
- 0,
- Object.keys(guildsMapped).length - 1
- );
-
- // Additional data
- const guildsSorted = sortedGuilds[1][0].reverse();
- const pages = guildsSorted.length;
- const argPage = interaction.options.getNumber("page", false);
- let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0;
-
- // Creating the embed
- const guild = guildsSorted[page];
- const subscriptionsTable = await getNewsTable(db);
- const subs = await subscriptionsTable?.get(`${guild.id}.subscriptions`).then(
- subs => subs?.length > 0 ? subs as string[] : [] as string[]
- ).catch(() => [] as string[]);
-
- let embed = await serverEmbed({
- guild,
- page: page + 1,
- pages,
- showInvite: true,
- showSubs: subs.length > 0,
- subs: subs.length
- });
-
- // Sending the embed
- const row = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId("left")
- .setEmoji("1137330341472915526")
- .setStyle(ButtonStyle.Primary),
- new ButtonBuilder()
- .setCustomId("right")
- .setEmoji("1137330125004869702")
- .setStyle(ButtonStyle.Primary)
- );
-
- await interaction.followUp({ embeds: [embed], components: [row] });
-
- // Listening for button events
- const collectorFilter = i => i.user.id === interaction.user.id;
- const collector = interaction.channel.createMessageComponentCollector({ filter: collectorFilter, time: 60000 });
- collector.on("collect", async interaction => {
- page = await handlePageUpdate(interaction, page, pages, row, guildsSorted);
- });
- }
-}
-
-async function handlePageUpdate(
- interaction: StringSelectMenuInteraction |
- UserSelectMenuInteraction |
- RoleSelectMenuInteraction |
- MentionableSelectMenuInteraction |
- ChannelSelectMenuInteraction |
- ButtonInteraction, page: number, pages: number, row, guildsSorted: any[]
-) {
- const db = await database();
-
- let embed;
- let guild;
- let subs;
-
- const subscriptionsTable = db.table("subscriptions");
- const subscriptions = await subscriptionsTable.all();
-
- switch (interaction.customId) {
- case "left":
- page--;
- if (page < 0) page = pages - 1;
- guild = guildsSorted[page];
- subs = subscriptions.filter(sub => (sub.value as string[] ?? [] as string[]).includes(guild.id));
-
- embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length });
- break;
- case "right":
- page++;
- if (page >= pages) page = 0;
- guild = guildsSorted[page];
- subs = subscriptions.filter(sub => (sub.value as string[]).includes(guild.id));
-
- embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length });
- break;
- }
-
- interaction.message.edit({ embeds: [embed], components: [row] });
- interaction.deferUpdate();
- return page;
-}
diff --git a/src/commands/nebula/subscribe.ts b/src/commands/nebula/subscribe.ts
deleted file mode 100644
index d04dfe9..0000000
--- a/src/commands/nebula/subscribe.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction, DMChannel
-} from "discord.js";
-import { genColor } from "../../utils/colorGen.js";
-import { getNewsTable } from "../../utils/database.js";
-import errorEmbed from "../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-type SubscriptionType = string[];
-
-export default class Subscribe {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("subscribe")
- .setDescription("Subscribe to the news of Nebula.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsTable = await getNewsTable(db);;
-
- const user = interaction.user;
-
- const nebulaId = "903852579837059113";
- const subscriptions = await newsTable?.get(`${nebulaId}.subscriptions`).then(
- (subscriptions: any) => subscriptions as SubscriptionType ?? [] as SubscriptionType
- ).catch(() => [] as SubscriptionType);
- const hasSub = subscriptions?.includes(user.id);
-
- const dmChannel = (await interaction.user.createDM().catch(() => null)) as DMChannel | null;
- if (!dmChannel) return await interaction.followUp({
- embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")]
- });
- const sendDms = await dmChannel?.send("You have updated the subscription status of \`Nebula\`.").catch(() => null);
- if (!sendDms) {
- await newsTable.pull(`${nebulaId}.subscriptions`, user.id);
- return await interaction.followUp({
- embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")]
- });
- }
-
- await newsTable[!hasSub ? "push" : "pull"](`${nebulaId}.subscriptions`, user.id);
-
- const subscriptionEmbed = new EmbedBuilder()
- .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} Nebula`)
- .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of Nebula.`)
- .setColor(genColor(100));
-
- await interaction.followUp({ embeds: [subscriptionEmbed] });
- }
-}
diff --git a/src/commands/news/Add.ts b/src/commands/news/Add.ts
new file mode 100644
index 0000000..4420702
--- /dev/null
+++ b/src/commands/news/Add.ts
@@ -0,0 +1,82 @@
+import {
+ ActionRowBuilder,
+ EmbedBuilder,
+ ModalBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { addNews, listAllQuery } from "../../utils/database/news";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { sendChannelNews } from "../../utils/sendChannelNews";
+
+export default class Add {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder().setName("add").setDescription("Add your news.");
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageGuild)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Server** permission."
+ );
+
+ const firstActionRow = new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("title")
+ .setPlaceholder("Write a title")
+ .setMaxLength(100)
+ .setStyle(TextInputStyle.Short)
+ .setLabel("Title")
+ .setRequired(true)
+ );
+
+ const secondActionRow = new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("body")
+ .setPlaceholder("Insert your content here")
+ .setMaxLength(4000)
+ .setStyle(TextInputStyle.Paragraph)
+ .setLabel("Content (supports Markdown)")
+ .setRequired(true)
+ );
+
+ const newsModal = new ModalBuilder()
+ .setCustomId("addnews")
+ .setTitle("Write your news.")
+ .addComponents(firstActionRow, secondActionRow);
+
+ await interaction.showModal(newsModal).catch(err => console.error(err));
+ interaction.client.once("interactionCreate", async i => {
+ if (!i.isModalSubmit()) return;
+
+ const id = (listAllQuery.all(guild.id).length + 1).toString();
+ addNews(
+ guild.id,
+ i.fields.getTextInputValue("title"),
+ i.fields.getTextInputValue("body"),
+ i.user.displayName,
+ i.user.avatarURL()!,
+ null!,
+ id
+ );
+
+ await sendChannelNews(guild, id, interaction).catch(err => console.error(err));
+ await i.reply({
+ embeds: [new EmbedBuilder().setTitle("News added.").setColor(genColor(100))],
+ ephemeral: true
+ });
+ });
+ }
+}
diff --git a/src/commands/news/Edit.ts b/src/commands/news/Edit.ts
new file mode 100644
index 0000000..48f3a0f
--- /dev/null
+++ b/src/commands/news/Edit.ts
@@ -0,0 +1,112 @@
+import {
+ ActionRowBuilder,
+ EmbedBuilder,
+ ModalBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+ type ChatInputCommandInteraction,
+ type Role,
+ type TextChannel
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { get, updateNews } from "../../utils/database/news";
+import { getSetting } from "../../utils/database/settings";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+import { sendChannelNews } from "../../utils/sendChannelNews";
+
+export default class Edit {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("edit")
+ .setDescription("Edits the news of your guild.")
+ .addStringOption(string =>
+ string
+ .setName("id")
+ .setDescription("The ID of the news you want to edit.")
+ .setRequired(true)
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageGuild)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Server** permission."
+ );
+
+ const id = interaction.options.getString("id")!;
+ const news = get(id);
+ if (!news) return await errorEmbed(interaction, "The specified news don't exist.");
+
+ const firstActionRow = new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("title")
+ .setMaxLength(100)
+ .setStyle(TextInputStyle.Short)
+ .setLabel("Title")
+ .setValue(news.title)
+ .setRequired(true)
+ );
+
+ const secondActionRow = new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("body")
+ .setMaxLength(4000)
+ .setStyle(TextInputStyle.Paragraph)
+ .setLabel("Content (supports Markdown)")
+ .setValue(news.body)
+ .setRequired(true)
+ );
+
+ const editModal = new ModalBuilder()
+ .setCustomId("editnews")
+ .setTitle(`Edit news: ${news.title}`)
+ .addComponents(firstActionRow, secondActionRow);
+
+ await interaction.showModal(editModal).catch(err => console.error(err));
+ interaction.client.once("interactionCreate", async i => {
+ if (!i.isModalSubmit()) return;
+
+ const role = getSetting(guild.id, "news", "role_id") as string;
+ let roleToSend: Role | undefined;
+ if (role) roleToSend = guild.roles.cache.get(role);
+ const title = i.fields.getTextInputValue("title");
+ const body = i.fields.getTextInputValue("body");
+
+ if (!getSetting(guild.id, "news", "edit_original_message"))
+ await sendChannelNews(guild, id, interaction, title, body);
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP })
+ .setTitle(title)
+ .setDescription(body)
+ .setTimestamp(parseInt(news.updatedAt.toString()) ?? null)
+ .setFooter({ text: `Edited news from ${guild.name}\nID: ${news.id}` })
+ .setColor(genColor(200));
+
+ (
+ guild.channels.cache.get(
+ (getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id
+ ) as TextChannel
+ )?.messages.edit(news.messageID, {
+ embeds: [embed],
+ content: roleToSend ? `<@&${roleToSend.id}>` : undefined
+ });
+
+ updateNews(id, title, body);
+ await interaction.reply({
+ embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))],
+ ephemeral: true
+ });
+ });
+ }
+}
diff --git a/src/commands/news/Remove.ts b/src/commands/news/Remove.ts
new file mode 100644
index 0000000..0cb5bee
--- /dev/null
+++ b/src/commands/news/Remove.ts
@@ -0,0 +1,55 @@
+import {
+ EmbedBuilder,
+ PermissionsBitField,
+ SlashCommandSubcommandBuilder,
+ TextChannel,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { deleteNews, get } from "../../utils/database/news";
+import { getSetting } from "../../utils/database/settings";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+
+export default class Remove {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("remove")
+ .setDescription("Removes news from your guild.")
+ .addStringOption(string =>
+ string
+ .setName("id")
+ .setDescription("The ID of the news. (found in the footer)")
+ .setRequired(true)
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ const guild = interaction.guild!;
+ if (
+ !guild.members.cache
+ .get(interaction.user.id)
+ ?.permissions.has(PermissionsBitField.Flags.ManageGuild)
+ )
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ "You need the **Manage Server** permission."
+ );
+
+ const id = interaction.options.getString("id")!;
+ const news = get(id);
+ if (!news) return await errorEmbed(interaction, "The specified news don't exist.");
+
+ const newsChannel = (await guild.channels
+ .fetch((getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id)
+ .catch(() => null)) as TextChannel;
+
+ if (newsChannel) await newsChannel.messages.delete(news.messageID);
+ deleteNews(id);
+ await interaction.reply({
+ embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))],
+ ephemeral: true
+ });
+ }
+}
diff --git a/src/commands/news/View.ts b/src/commands/news/View.ts
new file mode 100644
index 0000000..bfc2da4
--- /dev/null
+++ b/src/commands/news/View.ts
@@ -0,0 +1,92 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ EmbedBuilder,
+ SlashCommandSubcommandBuilder,
+ type ChatInputCommandInteraction
+} from "discord.js";
+import { genColor } from "../../utils/colorGen";
+import { listAllNews } from "../../utils/database/news";
+import { errorEmbed } from "../../utils/embeds/errorEmbed";
+
+export default class View {
+ data: SlashCommandSubcommandBuilder;
+ constructor() {
+ this.data = new SlashCommandSubcommandBuilder()
+ .setName("view")
+ .setDescription("View the news of this server.")
+ .addNumberOption(number =>
+ number.setName("page").setDescription("The page of the news that you want to see.")
+ );
+ }
+
+ async run(interaction: ChatInputCommandInteraction) {
+ let page = interaction.options.getNumber("page") ?? 1;
+ const news = listAllNews(interaction.guild?.id!);
+ const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt);
+
+ if (!news || !sortedNews || !sortedNews.length)
+ return await errorEmbed(
+ interaction,
+ "No news found.",
+ "Admins can add news with the **/news add** command."
+ );
+
+ if (page > sortedNews.length) page = sortedNews.length;
+ if (page < 1) page = 1;
+
+ function getEmbed() {
+ const currentNews = sortedNews[page - 1];
+ return new EmbedBuilder()
+ .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP })
+ .setTitle(currentNews.title)
+ .setDescription(currentNews.body)
+ .setImage(currentNews.imageURL || null)
+ .setTimestamp(parseInt(currentNews.updatedAt))
+ .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` })
+ .setColor(genColor(200));
+ }
+
+ const row = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("left")
+ .setEmoji("1298708251256291379")
+ .setStyle(ButtonStyle.Primary),
+ new ButtonBuilder()
+ .setCustomId("right")
+ .setEmoji("1298708281493160029")
+ .setStyle(ButtonStyle.Primary)
+ );
+
+ const reply = await interaction.reply({ embeds: [getEmbed()], components: [row] });
+ const collector = reply.createMessageComponentCollector({ time: 30000 });
+ collector.on("collect", async (i: ButtonInteraction) => {
+ if (i.message.id != (await reply.fetch()).id)
+ return await errorEmbed(
+ i,
+ "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that."
+ );
+
+ if (i.user.id != interaction.user.id)
+ return await errorEmbed(i, "You aren't the person who executed this command.");
+
+ collector.resetTimer({ time: 30000 });
+ switch (i.customId) {
+ case "left":
+ page--;
+ if (page < 1) page = sortedNews.length;
+ await i.update({ embeds: [getEmbed()], components: [row] });
+ break;
+ case "right":
+ page++;
+ if (page > sortedNews.length) page = 1;
+ await i.update({ embeds: [getEmbed()], components: [row] });
+ break;
+ }
+ });
+
+ collector.on("end", async () => await interaction.editReply({ components: [] }));
+ }
+}
diff --git a/src/commands/settings/command/list.ts b/src/commands/settings/command/list.ts
deleted file mode 100644
index b63d47e..0000000
--- a/src/commands/settings/command/list.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- PermissionsBitField,
- type ChatInputCommandInteraction,
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getSettingsTable } from "../../../utils/database.js";
-
-export default class List {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("list")
- .setDescription("Lists all disabled commands.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
-
- const member = interaction.guild.members.cache.get(interaction.user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild))
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")],
- });
-
- const disabledCommands = await settingsTable
- ?.get(`${interaction.guild.id}.disabledCommands`)
- .then((disabledCommands) => disabledCommands as any[] ?? [])
- .catch(() => []);
-
- const listEmbed = new EmbedBuilder()
- .setTitle("📃 • Disabled commands")
- .setDescription(
- !disabledCommands || disabledCommands?.length == 0
- ? "There are no disabled commands."
- : disabledCommands
- .map((command) => {
- const [commandName, subcommandName] = command.split("/");
- return `/${commandName}${subcommandName ? ` ${subcommandName}` : ""}`;
- })
- .join("\n")
- )
- .setColor(genColor(100));
-
- return await interaction.followUp({ embeds: [listEmbed] });
- }
-}
diff --git a/src/commands/settings/command/toggle.ts b/src/commands/settings/command/toggle.ts
deleted file mode 100644
index 0046127..0000000
--- a/src/commands/settings/command/toggle.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import Commands from "../../../handlers/commands.js";
-import { getSettingsTable } from "../../../utils/database.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class Toggle {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("toggle")
- .setDescription("Enables/disables a command.")
- .addStringOption(option => option
- .setName("command")
- .setDescription("Layout: \"/topcommand (subcommand) (group)\".")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
-
- const commands = new Commands(interaction.client);
- await commands.loadCommands();
-
- const commandPath = interaction.options.getString("command", true);
- let [commandName, subcommandName, subcommandGroupName] = commandPath.split(" ");
- commandName = commandName.replace("/", "");
- const disabledCommands = await settingsTable?.get(`${interaction.guild.id}.disabledCommands`).then(
- (disabledCommands) => disabledCommands as any[] ?? []
- ).catch(() => []);
-
- const hasCommand = (name: string, subcommand?: string, subcommandGroup?: string) => {
- return commands.commands.some(command =>
- command.name === name &&
- (!subcommand || command.options.some(opt => opt.name === subcommand)) &&
- (!subcommandGroup || command.options.some(
- opt => opt.type === "SUB_COMMAND_GROUP" && opt.options?.some(opt => opt.name === subcommandGroup)
- ))
- );
- };
-
- const member = interaction.guild.members.cache.get(interaction.user.id);
- const isEnabled = !disabledCommands.includes(commandName);
- const updatedDisabledCommands = !isEnabled
- ? disabledCommands.filter((cmd) => cmd !== commandName)
- : [...disabledCommands, commandName];
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild))
- return await interaction.followUp({
- embeds: [errorEmbed("You need the **Manage Server** permission to enable commands.")],
- });
-
- if (!hasCommand(commandName, subcommandName, subcommandGroupName)) {
- return await interaction.followUp({ embeds: [errorEmbed("The specified command doesn't exist.")] });
- }
-
- const embed = new EmbedBuilder()
- .setTitle(`⌚ • ${isEnabled ? "Disabling" : "Enabling"} ${commandPath}.`)
- .setDescription("The command hasn't been updated yet, we will edit this message once it has.")
- .setColor(genColor(100));
-
- interaction.followUp({ embeds: [embed] });
- await settingsTable.set(`${interaction.guild.id}.disabledCommands`, updatedDisabledCommands);
-
- commands.registerCommandsForGuild(interaction.guild, ...updatedDisabledCommands).then(() => {
- embed
- .setTitle(`✅ • ${isEnabled ? "Disabled" : "Enabled"} ${commandPath}.`)
- .setDescription(`The command has been ${isEnabled ? "disabled." : "enabled."}`);
-
- interaction.editReply({ embeds: [embed] });
- });
- }
-}
diff --git a/src/commands/settings/leveling/block-channels.ts b/src/commands/settings/leveling/block-channels.ts
deleted file mode 100644
index 64bd32e..0000000
--- a/src/commands/settings/leveling/block-channels.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction,
- ChannelType,
- Collection,
- NonThreadGuildBasedChannel
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class BlockChannels {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("block-channels")
- .setDescription("Toggles blocked channels for leveling, no options = view blocked.")
- .addChannelOption(option => option
- .setName("channel")
- .setDescription("A channel to toggle blocked status in.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
-
- const guild = interaction.guild;
- const channel = interaction.options.getChannel("channel");
- const member = interaction.guild.members.cache.get(interaction.user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to set a channel.")]
- });
- }
-
- const blocked = await settingsTable?.get(`${guild.id}.leveling.blockedchannels`).then(
- blocked => blocked as any[] ?? []
- ).catch(() => []);
- if (!channel) {
- return await interaction.followUp({
- embeds: [
- new EmbedBuilder()
- .setTitle("📃 • Blocked Leveling Channels")
- .setDescription(
- blocked?.length == 0 ?
- "There are no blocked channels." :
- `${blocked?.map(id => `- <#${id}>`).join("\n")}`
- )
- .setColor(genColor(100))
- ]
- });
- }
-
- if (channel?.type != ChannelType.GuildText) {
- return await interaction.followUp({
- embeds: [errorEmbed("You must provide a text channel.")]
- });
- }
-
- const perms = (await interaction.guild.channels.fetch().then(
- (channels: Collection) => channels as Collection
- )).get(channel.id)?.permissionsFor(guild.members.me);
- if (!perms.has(PermissionsBitField.Flags.SendMessages)) {
- return await interaction.followUp({
- embeds: [errorEmbed("I don't have permission to send messages in that channel.")]
- });
- }
-
- const isBlocked = blocked?.includes(channel.id);
- await settingsTable[isBlocked ? "pull" : "push"](`${interaction.guild.id}.leveling.blockedchannels`, channel.id);
-
- await interaction.followUp({
- embeds: [
- new EmbedBuilder()
- .setTitle("✅ • Blocked Leveling Channels")
- .setDescription(
- isBlocked ?
- `Unblocked <#${channel.id}>.` :
- `Blocked <#${channel.id}>.`
- )
- .setColor(genColor(100))
- ]
- });
- }
-}
diff --git a/src/commands/settings/leveling/channel.ts b/src/commands/settings/leveling/channel.ts
deleted file mode 100644
index 32f49cd..0000000
--- a/src/commands/settings/leveling/channel.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction,
- ChannelType,
- Collection,
- NonThreadGuildBasedChannel
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Channel {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("channel")
- .setDescription("Sets the channel for level up messages (no channel = no messages).")
- .addChannelOption(option => option
- .setName("channel")
- .setDescription("The channel where level up messages are sent in.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
-
- const guild = interaction.guild;
- const user = interaction.user;
- const channel = interaction.options.getChannel("channel");
- const member = interaction.guild.members.cache.get(user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to set a channel.")]
- });
- }
-
- if (!channel) {
- await settingsTable?.delete(`${guild.id}.leveling.channel`).catch(() => "");
-
- return await interaction.followUp({
- embeds: [
- new EmbedBuilder()
- .setTitle("✅ • Leveling Channel")
- .setDescription("Level up messages will no longer be sent.")
- .setColor(genColor(100))
- ]
- });
- }
-
- if (channel?.type != ChannelType.GuildText) {
- return await interaction.followUp({
- embeds: [errorEmbed("You must provide a text channel.")]
- });
- }
-
- const perms = (await guild.channels.fetch().then(
- (channels: Collection) => channels as Collection
- )).get(channel.id).permissionsFor(guild.members.me);
- if (!perms.has(PermissionsBitField.Flags.SendMessages)) {
- return await interaction.followUp({
- embeds: [errorEmbed("I don't have permission to send messages in that channel.")]
- });
- }
-
- await settingsTable.set(`${guild.id}.leveling.channel`, channel.id).catch(() => "");
-
- await interaction.followUp({
- embeds: [
- new EmbedBuilder()
- .setTitle("✅ • Leveling Channel")
- .setDescription(`Level up messages will now be sent in <#${channel.id}>.`)
- .setColor(genColor(100))
- ]
- });
- }
-}
diff --git a/src/commands/settings/leveling/rewards.ts b/src/commands/settings/leveling/rewards.ts
deleted file mode 100644
index 4c9e5da..0000000
--- a/src/commands/settings/leveling/rewards.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction,
- Role,
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { getSettingsTable } from "../../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export type Reward = {
- roleId: string,
- level: number,
-}
-
-export default class Rewards {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("rewards")
- .setDescription("Sets/gets reward roles for each level -> No options = shows.")
- .addNumberOption(option => option
- .setName("level")
- .setDescription("The level to set the reward role for. When set without role option => deletes reward.")
- )
- .addRoleOption(option => option
- .setName("role")
- .setDescription("The role that should be awarded for the level => leaving empty = deletes reward for specified level.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
-
- const inputLevel = interaction.options.getNumber("level", false);
- const inputRole = interaction.options.getRole("role", false) as Role | null;
-
- if (!inputLevel && !inputRole) {
- // List the rewards
- const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a.level - b.level)) as Reward[];
- if (rewards.length == 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("There are no rewards set for this server.")]
- });
- }
-
- const rewardsEmbed = new EmbedBuilder()
- .setTitle("🎁 • Rewards")
- .setDescription("Here are the rewards set for this server.")
- .setColor(genColor(100));
-
- for (const { roleId, level } of rewards) {
- if (!roleId) continue;
- rewardsEmbed.addFields([{
- name: `Level ${level}`,
- value: `<@&${roleId}>`,
- }]);
- }
-
- return await interaction.followUp({
- embeds: [rewardsEmbed]
- });
- } else if (inputLevel && !inputRole) {
- // Delete a reward
- const level = Number(inputLevel);
- const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a?.level - b?.level)) as Reward[];
- if (rewards.length == 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("There are no rewards set for this server.")]
- });
- }
-
- const reward = rewards.find(reward => reward?.level == level);
- if (!reward) {
- return await interaction.followUp({
- embeds: [errorEmbed(`There is no reward set for level ${level}.`)]
- });
- }
-
- const newRewards = rewards.filter(reward => reward.level != level);
- await settingsTable?.set(`${interaction.guild.id}.leveling.rewards`, newRewards).catch(() => []);
-
- return await interaction.followUp({
- embeds: [new EmbedBuilder()
- .setTitle("🎁 • Rewards")
- .setDescription(`Deleted the reward for level ${level}.`)
- .setColor(genColor(100))
- ]
- });
- }
-
- // Set a reward
- const level = Number(inputLevel);
- const role = await interaction.guild.roles.fetch(String(inputRole?.id)).catch(() => null);
-
- if (!role) {
- return await interaction.followUp({
- embeds: [errorEmbed("That role doesn't exist or couldn't be loaded.")]
- });
- }
-
- const permissions = role?.permissions as PermissionsBitField;
- if (level < 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("You can't set a reward for a negative level.")]
- });
- }
- if (level == 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("You can't set a reward for level 0.")]
- });
- }
- if (role.position >= interaction.guild.members.me.roles.highest.position) {
- return await interaction.followUp({
- embeds: [errorEmbed("That role is above mine.")]
- });
- }
- if (role?.name?.includes("everyone")) {
- return await interaction.followUp({
- embeds: [errorEmbed("I can't give out the @everyone role.")]
- });
- }
- if (
- permissions.has(PermissionsBitField.Flags.Administrator) ||
- permissions.has(PermissionsBitField.Flags.ManageRoles) ||
- permissions.has(PermissionsBitField.Flags.ManageGuild) ||
- permissions.has(PermissionsBitField.Flags.ManageChannels) ||
- permissions.has(PermissionsBitField.Flags.ManageWebhooks) ||
- permissions.has(PermissionsBitField.Flags.ManageGuildExpressions) ||
- permissions.has(PermissionsBitField.Flags.ManageMessages) ||
- permissions.has(PermissionsBitField.Flags.ManageThreads) ||
- permissions.has(PermissionsBitField.Flags.ManageNicknames)
- ) {
- return await interaction.followUp({
- embeds: [errorEmbed("I can't give out a role with dangerous permissions.")]
- });
- }
-
- const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a?.level - b?.level)) as Reward[];
- const newRewards = rewards.filter(reward => reward.level != level);
- newRewards.push({
- roleId: role?.id,
- level: level
- });
- await settingsTable?.set(`${interaction.guild.id}.leveling.rewards`, newRewards).catch(() => "");
-
- return await interaction.followUp({
- embeds: [new EmbedBuilder()
- .setTitle("🎁 • Rewards")
- .setDescription(`Set the reward for level ${level} to <@&${role?.id}>.`)
- .setColor(genColor(100))
- ]
- });
- }
-
- async getRewards(guildId: string): Promise {
- const settingsTable = await getSettingsTable(this.db);
- const rewards = new Promise((resolve, reject) => {
- settingsTable?.get(`${guildId}.leveling.rewards`).then(rewards => {
- if (!rewards) return resolve([]);
- resolve(rewards);
- }).catch(() => {
- resolve([]);
- });
- });
- return rewards as Promise;
- }
-}
diff --git a/src/commands/settings/leveling/set.ts b/src/commands/settings/leveling/set.ts
deleted file mode 100644
index d7e1e93..0000000
--- a/src/commands/settings/leveling/set.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder,
- type ChatInputCommandInteraction,
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { getLevelingTable, getSettingsTable } from "../../../utils/database.js";
-import { QuickDB } from "quick.db";
-import { Reward } from "./rewards.js";
-
-export default class Set {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("set")
- .setDescription("Sets the levels for a user.")
- .addUserOption(option => option
- .setName("user")
- .setDescription("The user to set the levels for.")
- .setRequired(true)
- )
- .addNumberOption(option => option
- .setName("levels")
- .setDescription("The amount of levels to set the user to.")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const levelingTable = await getLevelingTable(db);
- const settingsTable = await getSettingsTable(db);
-
- const target = interaction.options.getUser("user", true);
- const level = interaction.options.getNumber("levels", true);
-
- if (level < 0) {
- return await interaction.followUp({
- embeds: [errorEmbed("You can't set a user's levels to a negative number.")]
- });
- }
-
- await levelingTable.set(`${interaction.guild.id}.${target.id}`, {
- levels: level,
- exp: 0
- }).catch(() => "");
-
- const levelRewards = await settingsTable?.get(`${interaction.guild.id}.leveling.rewards`).then(
- (data) => {
- if (!data) return [] as Reward[];
- return data as Reward[] ?? [] as Reward[];
- }
- ).catch(() => [] as Reward[]);
- const members = await interaction.guild.members.fetch();
- for (const { level: rewardLevel, roleId } of levelRewards) {
- const role = interaction.guild.roles.cache.get(roleId);
-
- if (level >= rewardLevel) {
- await members.get(target.id)?.roles.add(role);
- continue;
- }
-
- await members.get(target.id)?.roles.remove(role);
- }
-
- await interaction.followUp({
- embeds: [
- new EmbedBuilder()
- .setTitle("✅ • Levels set!")
- .setDescription(`Set ${target}'s levels to ${level}.`)
- .setColor(genColor(100))
- ]
- });
- }
-}
diff --git a/src/commands/settings/leveling/toggle.ts b/src/commands/settings/leveling/toggle.ts
deleted file mode 100644
index 9e064dd..0000000
--- a/src/commands/settings/leveling/toggle.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder,
- type ChatInputCommandInteraction,
- PermissionsBitField,
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import database from "../../../utils/database.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-
-export default class Toggle {
- data: SlashCommandSubcommandBuilder;
- constructor() {
- this.data = new SlashCommandSubcommandBuilder()
- .setName("toggle")
- .setDescription("Toggles if leveling is enabled.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = await database();
-
- const enabled = await db.table("settings")?.get(`${interaction.guild.id}.leveling.enabled`).then(
- (enabled) => !!enabled
- ).catch(() => false);
- await db.table("settings").set(`${interaction.guild.id}.leveling.enabled`, !enabled);
-
- const user = (await interaction.guild.members.me.fetch())
- if (!user.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to toggle leveling.")]
- });
- }
-
- await interaction.followUp({
- embeds: [
- new EmbedBuilder()
- .setTitle("✅ • Leveling toggled!")
- .setDescription(`Leveling is now ${enabled ? "disabled" : "enabled"}.`)
- .setColor(genColor(100))
- ]
- });
- }
-}
diff --git a/src/commands/settings/moderation/logs.ts b/src/commands/settings/moderation/logs.ts
deleted file mode 100644
index 0dae559..0000000
--- a/src/commands/settings/moderation/logs.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import {
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- PermissionsBitField,
- type ChatInputCommandInteraction,
- ChannelType,
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-import { getSettingsTable } from "../../../utils/database.js";
-
-export default class Logs {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("logs")
- .setDescription("Sets/Remove the logs channel.")
- .addChannelOption(option =>
- option.setName("channel")
- .setDescription("Where to send logs in. Empty = deleted.")
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const settingsTable = await getSettingsTable(db);
-
- const member = interaction.guild.members.cache.get(interaction.user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")],
- });
- }
-
- const specifiedChannel = interaction.options.getChannel("channel");
- if (specifiedChannel?.type != ChannelType.GuildText) return await interaction.followUp({ embeds: [errorEmbed("You must provide a text channel.")] });
-
- if (!specifiedChannel?.id) await settingsTable?.delete(`${interaction.guild.id}.logChannel`);
- else await settingsTable?.set(`${interaction.guild.id}.logChannel`, specifiedChannel?.id)
-
- const listEmbed = new EmbedBuilder()
- .setTitle("📃 • Log channel")
- .setDescription(`The log channel has been ${specifiedChannel?.id ? `set to <#${specifiedChannel.id}>` : "deleted"}.`)
- .setColor(genColor(100));
-
- return await interaction.followUp({ embeds: [listEmbed] });
- }
-}
diff --git a/src/commands/settings/news/add.ts b/src/commands/settings/news/add.ts
deleted file mode 100644
index e9072d4..0000000
--- a/src/commands/settings/news/add.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction, ModalBuilder, TextInputBuilder,
- ActionRowBuilder, TextInputStyle
-} from "discord.js";
-import { getNewsTable } from "../../../utils/database.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import sendSubscribedNews, { News } from "../../../utils/sendSubscribedNews.js";
-import sendChannelNews from "../../../utils/sendChannelNews.js";
-import validateURL from "../../../utils/validateURL.js";
-import { QuickDB } from "quick.db";
-
-export default class Add {
- data: SlashCommandSubcommandBuilder;
- deferred: boolean = false;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("add")
- .setDescription("Adds news to your guild.");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsTable = await getNewsTable(db);
-
- const user = interaction.user;
- const guild = interaction.guild;
-
- const author = user.displayName ?? user.username;
- const timestamp = Date.now().toString();
- const member = guild.members.cache.get(user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.reply({
- embeds: [errorEmbed("You need **Manage Server** permissions to add news.")],
- });
- }
-
- const newsModal = new ModalBuilder()
- .setCustomId("addnews")
- .setTitle("Create new News for your server/project");
-
- const titleInput = new TextInputBuilder()
- .setCustomId("title")
- .setPlaceholder("Title")
- .setStyle(TextInputStyle.Short)
- .setMaxLength(100)
- .setLabel("Title")
- .setRequired(true);
-
- const bodyInput = new TextInputBuilder()
- .setCustomId("body")
- .setPlaceholder("Content (markdown)")
- .setMaxLength(4000)
- .setStyle(TextInputStyle.Paragraph)
- .setLabel("Content (markdown)")
- .setRequired(true);
-
- const imageURLInput = new TextInputBuilder()
- .setCustomId("imageurl")
- .setPlaceholder("Big image URL (bottom)")
- .setStyle(TextInputStyle.Short)
- .setMaxLength(1000)
- .setLabel("Big image URL (bottom)")
- .setRequired(false);
-
- const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder;
- const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder;
- const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder;
-
- newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow);
- await interaction.showModal(newsModal).catch((err) => {
- console.error(err);
- });
-
- interaction.client.once("interactionCreate", async (interaction) => {
- if (!interaction.isModalSubmit()) return;
- if (interaction.customId !== "addnews") return;
-
- const title = interaction.fields.getTextInputValue("title") as string;
- const body = interaction.fields.getTextInputValue("body") as string;
- const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined;
- let validURL = false;
- if (imageURL) validURL = validateURL(imageURL);
-
- if (!validURL && imageURL) {
- await interaction.reply({
- embeds: [errorEmbed("The image URL you provided is invalid.")],
- });
- return;
- }
-
- const id = crypto.randomUUID();
- const news = {
- id,
- title,
- body,
- imageURL,
- author,
- authorPfp: interaction.user.avatarURL(),
- createdAt: timestamp,
- updatedAt: timestamp,
- messageId: null
- };
-
- sendSubscribedNews(interaction.guild, news as News).catch((err) => {
- console.error(err);
- });
- sendChannelNews(interaction.guild, news as News, id).catch((err) => {
- console.error(err);
- });
-
- const embed = new EmbedBuilder()
- .setTitle("✅ • News sent!")
- .setColor(genColor(100));
-
- await newsTable.set(`${guild.id}.news.${id}`, news);
- await interaction.reply({ embeds: [embed] });
- });
- }
-}
diff --git a/src/commands/settings/news/channel.ts b/src/commands/settings/news/channel.ts
deleted file mode 100644
index 5803a7b..0000000
--- a/src/commands/settings/news/channel.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import {
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- PermissionsBitField,
- type ChatInputCommandInteraction,
- ChannelType,
-} from "discord.js";
-import { getNewsTable } from "../../../utils/database.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class Channel {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("channel")
- .setDescription("Sets/removes (when no options) a news channel, all news you post will be sent.")
- .addChannelOption((option) => option.setName("channel").setDescription("The channel to send news to."))
- .addRoleOption((option) => option.setName("role").setDescription("The role to ping when news are sent."));
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsTable = await getNewsTable(db);
-
- const guild = interaction.guild;
- const channelOption = interaction.options.getChannel("channel");
- const roleOption = interaction.options.getRole("role");
-
- const channel = guild.channels.cache.get(channelOption?.id);
- const role = guild.roles.cache.get(roleOption?.id ?? "");
-
- if (role?.name === "@everyone") {
- return await interaction.followUp({ embeds: [errorEmbed("You can't ping @everyone.")] });
- }
- if (!channelOption && !roleOption) {
- await newsTable.delete(`${guild.id}.channel`);
- return await interaction.followUp({
- embeds: [
- new EmbedBuilder().setTitle("✅ • Removed news channel.")
- ]
- });
- }
-
- if (!interaction.memberPermissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to add news.")],
- });
- }
- if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) {
- return await interaction.followUp({ embeds: [errorEmbed("The channel must be a text channel.")] });
- }
-
- await newsTable.set(`${guild.id}.channel`, {
- channelId: channel.id,
- roleId: role?.id ?? "",
- });
-
- const embed = new EmbedBuilder()
- .setTitle("✅ • News channel set!")
- .setColor(genColor(100));
-
- if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`);
- else embed.setDescription(`No role will be pinged when news are sent.`);
-
- await interaction.followUp({ embeds: [embed] });
- }
-}
diff --git a/src/commands/settings/news/edit.ts b/src/commands/settings/news/edit.ts
deleted file mode 100644
index b6ed09a..0000000
--- a/src/commands/settings/news/edit.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import {
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- PermissionsBitField,
- type ChatInputCommandInteraction,
- ModalBuilder,
- TextInputBuilder,
- ActionRowBuilder,
- TextInputStyle,
- TextChannel,
- Message,
-} from "discord.js";
-import { getNewsTable } from "../../../utils/database.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import sendSubscribedNewsletter, { News } from "../../../utils/sendSubscribedNews.js";
-import validateURL from "../../../utils/validateURL.js";
-import { QuickDB } from "quick.db";
-
-export default class Edit {
- data: SlashCommandSubcommandBuilder;
- deferred: boolean = false;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("edit")
- .setDescription("Edits new of your guild.")
- .addStringOption((option) =>
- option
- .setName("id")
- .setDescription("The ID of the news you want to edit.")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsTable = await getNewsTable(db);
-
- const user = interaction.user;
- const guild = interaction.guild;
- const id = interaction.options.getString("id", true).trim();
- const news = await newsTable?.get(`${guild.id}.news.${id}`).then(
- (news) => news as News
- ).catch(() => null as News | null);
-
- if (!news) return await interaction.followUp({ embeds: [errorEmbed("The specified news doesn't exist.")] });
-
- const author = user.displayName ?? user.username;
- const timestamp = Date.now().toString();
- const member = guild.members.cache.get(user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to add news.")],
- });
- }
-
- const editModal = new ModalBuilder()
- .setCustomId("editnews")
- .setTitle("Edit News: " + news.title);
-
- const titleInput = new TextInputBuilder()
- .setCustomId("title")
- .setPlaceholder("Title")
- .setStyle(TextInputStyle.Short)
- .setMaxLength(100)
- .setLabel("Title")
- .setValue(news.title)
- .setRequired(true);
-
- const bodyInput = new TextInputBuilder()
- .setCustomId("body")
- .setPlaceholder("Content (markdown)")
- .setMaxLength(4000)
- .setStyle(TextInputStyle.Paragraph)
- .setLabel("Content (markdown)")
- .setValue(news.body)
- .setRequired(true);
-
- const imageURLInput = new TextInputBuilder()
- .setCustomId("imageurl")
- .setPlaceholder("Big image URL (bottom)")
- .setStyle(TextInputStyle.Short)
- .setMaxLength(1000)
- .setLabel("Big image URL (bottom)")
- .setValue(news.imageURL)
- .setRequired(false);
-
- const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder;
- const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder;
- const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder;
-
- editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow);
- await interaction.showModal(editModal).catch((err) => {
- console.error(err);
- });
-
- interaction.client.once("interactionCreate", async (interaction) => {
- if (!interaction.isModalSubmit()) return;
- if (interaction.customId !== "editnews") return;
-
- const title = interaction.fields.getTextInputValue("title") as string;
- const body = interaction.fields.getTextInputValue("body") as string;
- const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined;
- let validURL = false;
- if (imageURL) validURL = validateURL(imageURL);
-
- if (!validURL && imageURL) {
- await interaction.reply({
- embeds: [errorEmbed("The image URL you provided is invalid.")],
- });
- return;
- }
-
- const newNews = {
- ...news,
- title,
- body,
- imageURL,
- author,
- authorPfp: user.avatarURL(),
- updatedAt: timestamp
- };
-
- sendSubscribedNewsletter(guild, {
- ...newNews,
- title: `Updated: ${newNews.title}`,
- } as News).catch((err) => {
- console.error(err);
- });
-
- const newsEmbed = new EmbedBuilder()
- .setAuthor({ name: newNews.author, iconURL: newNews.authorPfp ?? null })
- .setTitle(newNews.title)
- .setDescription(newNews.body)
- .setImage(newNews.imageURL || null)
- .setTimestamp(parseInt(newNews.updatedAt))
- .setFooter({ text: `Updated news from ${guild.name}` })
- .setColor(genColor(200));
-
- const subscribedNewsChannel = await newsTable?.get(`${guild.id}.channel`).then(
- (channel) => channel as { channelId: string; roleId: string } | null
- ).catch(() => {
- return {
- channelId: null as string | null,
- roleId: null as string | null
- };
- });
- if (Boolean(subscribedNewsChannel.channelId)) {
- const messageId = newNews?.messageId;
- const newsChannel = (await guild.channels.fetch(subscribedNewsChannel?.channelId ?? "").catch(() => { })) as TextChannel | null;
-
- if (!messageId && newsChannel.id) {
- newNews.messageId = ((await newsChannel?.send({
- embeds: [newsEmbed],
- content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null
- }).catch(() => { })) as Message | null)?.id;
- } else if (newsChannel.id) {
- await newsChannel?.messages.edit(messageId, {
- embeds: [newsEmbed],
- content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null
- }).catch(() => { });
- }
- }
-
- const embed = new EmbedBuilder()
- .setTitle("✅ • News edited!")
- .setColor(genColor(100));
-
- await newsTable.set(`${guild.id}.news.${id}`, newNews);
- await interaction.reply({ embeds: [embed] });
- });
- }
-}
diff --git a/src/commands/settings/news/remove.ts b/src/commands/settings/news/remove.ts
deleted file mode 100644
index 934fc3a..0000000
--- a/src/commands/settings/news/remove.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import {
- SlashCommandSubcommandBuilder,
- EmbedBuilder,
- PermissionsBitField,
- type ChatInputCommandInteraction,
- TextChannel,
-} from "discord.js";
-import { getNewsTable } from "../../../utils/database.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { QuickDB } from "quick.db";
-
-export default class Remove {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("remove")
- .setDescription("Removes news from your guild.")
- .addStringOption((option) =>
- option
- .setName("id")
- .setDescription("The id of the news (can be found in the footer of the news).")
- .setRequired(true)
- );
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const newsTable = await getNewsTable(db);
-
- const user = interaction.user;
- const guild = interaction.guild;
- const providedId = interaction.options.getString("id");
- const member = guild.members.cache.get(user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")],
- });
- }
-
- const embed = new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100));
-
- const subscribedChannel = await newsTable?.get(`${guild.id}.channel`).then(
- (channel) => channel as { channelId: string, roleId: string }
- ).catch(() => {
- return {
- channelId: null,
- roleId: null
- }
- });
- const news = await newsTable?.get(providedId).catch(() => null);
- if (!news) return await interaction.followUp({ embeds: [errorEmbed("The specified news doesn't exist.")] });
-
- const messageId = news?.messageId;
- const newsChannel = (await interaction.guild.channels.fetch(subscribedChannel?.channelId ?? "").catch(() => null)) as TextChannel | null;
- if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null);
-
- await newsTable?.delete(`${guild.id}.news.${providedId}`).catch(() => null);
- await interaction.followUp({ embeds: [embed] });
- }
-}
diff --git a/src/commands/settings/serverboard/invite.ts b/src/commands/settings/serverboard/invite.ts
deleted file mode 100644
index 920b2d6..0000000
--- a/src/commands/settings/serverboard/invite.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import { getServerboardTable } from "../../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Toggle {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("invite")
- .setDescription("Toggles the invite link to your server in /info serverboard (Auto generates/deletes invites).");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const serverbTable = await getServerboardTable(db);
-
- const rulesChannel = interaction.guild?.rulesChannel;
- const guild = interaction.guild;
- const member = guild.members.cache.get(interaction.user.id);
-
- if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild))
- return await interaction.followUp({ embeds: [errorEmbed("You need **Manage Server** permissions to add an invite.")] });
-
- const invite = await serverbTable?.get(`${guild.id}.invite`).then(
- (invite) => String(invite)
- ).catch(() => "");
- if (Boolean(invite)) {
- // Delete invite
- let invites = await rulesChannel?.fetchInvites();
- if (!rulesChannel) invites = await guild.invites.fetch();
-
- if (invite) {
- await invites.find(inv => inv.url == invite)
- .delete("Serverboard invite disabled");
- }
-
- await serverbTable.delete(`${guild.id}.invite`);
-
- const embed = new EmbedBuilder()
- .setTitle("✅ • Invite deleted!")
- .setColor(genColor(100));
-
- return await interaction.followUp({ embeds: [embed] });
- }
-
- if (!rulesChannel) return await interaction.followUp({ embeds: [errorEmbed("You need a **rules channel** to create an invite.")] });
-
- const newInvite = await rulesChannel.createInvite({
- maxAge: 0,
- maxUses: 0,
- reason: "Serverboard invite enabled"
- });
- await serverbTable.set(`${guild.id}.invite`, newInvite.url);
-
- const embed = new EmbedBuilder()
- .setTitle("✅ • Invite created!")
- .setColor(genColor(100));
-
- await interaction.followUp({ embeds: [embed] });
- }
-}
diff --git a/src/commands/settings/serverboard/reveal.ts b/src/commands/settings/serverboard/reveal.ts
deleted file mode 100644
index 5718317..0000000
--- a/src/commands/settings/serverboard/reveal.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField,
- type ChatInputCommandInteraction
-} from "discord.js";
-import { genColor } from "../../../utils/colorGen.js";
-import errorEmbed from "../../../utils/embeds/errorEmbed.js";
-import database, { getServerboardTable } from "../../../utils/database.js";
-import { QuickDB } from "quick.db";
-
-export default class Reveal {
- data: SlashCommandSubcommandBuilder;
- db: QuickDB;
-
- constructor(db?: QuickDB) {
- this.db = db;
- this.data = new SlashCommandSubcommandBuilder()
- .setName("reveal")
- .setDescription("Toggles the shown status of your server, community = shown, non = hidden (default).");
- }
-
- async run(interaction: ChatInputCommandInteraction) {
- const db = this.db;
- const serverbTable = await getServerboardTable(db);
-
- const guild = interaction.guild;
- const isCommunity = guild?.rulesChannelId != null;
-
- const shown = await serverbTable?.get(`${guild?.id}.shown`).then(
- (shown) => !!shown
- ).catch(() => false);
- const member = guild?.members.cache.get(interaction.user.id);
-
- if (!member?.permissions.has(PermissionsBitField.Flags.ManageGuild)) {
- return await interaction.followUp({
- embeds: [errorEmbed("You need **Manage Server** permissions to toggle the shown status of your server.")],
- });
- }
-
- if (!isCommunity)
- await serverbTable.set(`${guild?.id}.shown`, !!shown).catch(() => { });
- else
- await serverbTable.set(`${guild?.id}.shown`, !shown).catch(() => { });
-
- const newShown = await serverbTable?.get(`${guild?.id}.shown`).then(
- (shown) => !!shown
- ).catch(() => false);
-
- const embed = new EmbedBuilder()
- .setTitle("✅ • Shown status toggled!")
- .setDescription(`Your server is now ${newShown ? "shown" : "hidden"}.`)
- .setColor(genColor(100));
-
- await interaction.followUp({ embeds: [embed] });
- }
-}
diff --git a/src/events/easterEggs.ts b/src/events/easterEggs.ts
deleted file mode 100644
index 6046dde..0000000
--- a/src/events/easterEggs.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Message } from "discord.js";
-import { pathToFileURL } from "url";
-import { join } from "path";
-import { readdirSync } from "fs";
-
-export default {
- name: "messageCreate",
- event: class MessageCreate {
- async run(message: Message) {
- if (message.author.bot) return;
- if (message.guildId !== "903852579837059113" && message.guildId !== "986268144446341142") return;
- const eventsPath = join(process.cwd(), "src", "events");
-
- for (const easterEggFile of readdirSync(join(eventsPath, "easterEggs"))) {
- const msg = await import(pathToFileURL(join(eventsPath, "easterEggs", easterEggFile)).toString());
- new msg.default().run(message, ...message.content);
- }
- }
- },
-};
diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts
deleted file mode 100644
index 0f30abc..0000000
--- a/src/events/easterEggs/Bread.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Message } from "discord.js";
-import multiReact from "../../utils/multiReact.js";
-
-export default class Bread {
- async run(message: Message) {
- const gif = "https://tenor.com/bOMAb.gif";
- const randomizedChance = Math.round(Math.random() * 100);
- let oddsForGif = 0.25;
-
- const breadSplit = message.content.toLowerCase().split("bread");
- if (breadSplit[1] == null) return;
-
- if (
- ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && breadSplit[1].startsWith(" ")) ||
- message.content.toLowerCase() === "bread"
- ) {
- if (randomizedChance <= oddsForGif) message.channel.send(gif);
- else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍");
- }
- }
-}
diff --git a/src/events/easterEggs/Fan.ts b/src/events/easterEggs/Fan.ts
deleted file mode 100644
index c48099b..0000000
--- a/src/events/easterEggs/Fan.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Message } from "discord.js";
-import randomise from "../../utils/randomise.js";
-
-export default class Fan {
- async run(message: Message) {
- if (message.content.toLowerCase().includes("i'm a big fan")) {
- const gifs = randomise([
- "https://tenor.com/bC37i.gif",
- "https://tenor.com/view/fan-gif-20757784",
- "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715",
- ]);
-
- await message.channel.send(gifs);
- }
- }
-}
diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts
deleted file mode 100644
index 2fd283c..0000000
--- a/src/events/easterEggs/Fireship.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Message } from "discord.js";
-
-export default class FireShip {
- async run(message: Message) {
- const content = message.content.toLowerCase();
-
- if (
- content.startsWith("this has been") &&
- content.endsWith("in 100 seconds") &&
- message.content !== "this has been in 100 seconds"
- )
- await message.channel.send(
- "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one"
- );
- }
-}
diff --git a/src/events/easterEggs/Honk.ts b/src/events/easterEggs/Honk.ts
deleted file mode 100644
index 1003288..0000000
--- a/src/events/easterEggs/Honk.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { Message } from "discord.js";
-
-export default class Honk {
- async run(message: Message) {
- const honks: string[] = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"];
- if (honks.includes(message.content.toLowerCase())) message.channel.send("https://tenor.com/bW8sm.gif");
- }
-}
diff --git a/src/events/easterEggs/WhoPinged.ts b/src/events/easterEggs/WhoPinged.ts
deleted file mode 100644
index 94b5d5d..0000000
--- a/src/events/easterEggs/WhoPinged.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Message } from "discord.js";
-import randomise from "../../utils/randomise.js";
-
-export default class WhoPinged {
- async run(message: Message) {
- if (message.content.toLowerCase().includes(`<@${message.client.user.id}>`)) {
- const gifs = randomise([
- "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356",
- "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226",
- "https://tenor.com/view/who-pinged-me-ping-discord-discord-ping-undertaker-gif-20399650",
- "https://tenor.com/view/who-pinged-me-ping-tudou-mr-potato-cat-gif-22762448",
- "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322",
- "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406",
- "https://tenor.com/view/tense-table-smash-mad-gif-13656077",
- "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175",
- ]);
-
- await message.channel.send(gifs);
- }
- }
-}
diff --git a/src/events/easterEggs/amerikaYa.ts b/src/events/easterEggs/amerikaYa.ts
new file mode 100644
index 0000000..99ae9a2
--- /dev/null
+++ b/src/events/easterEggs/amerikaYa.ts
@@ -0,0 +1,12 @@
+import type { Message, TextChannel } from "discord.js";
+import { randomise } from "../../utils/randomise";
+
+export default async function run(message: Message) {
+ if (message.content.toLowerCase() != "amerika ya") return;
+ const response = randomise([
+ "HALLO :D HALLO :D HALLO :D HALLO :D",
+ "https://tenor.com/view/america-ya-gif-15374592095658975433"
+ ]);
+
+ await (message.channel as TextChannel).send(response);
+}
diff --git a/src/events/easterEggs/bread.ts b/src/events/easterEggs/bread.ts
new file mode 100644
index 0000000..d7fd9a6
--- /dev/null
+++ b/src/events/easterEggs/bread.ts
@@ -0,0 +1,10 @@
+import type { Message, TextChannel } from "discord.js";
+import { multiReact } from "../../utils/multiReact";
+
+export default async function run(message: Message) {
+ if (!message.content.toLowerCase().includes("bread")) return;
+
+ if (Math.round(Math.random() * 100) <= 0.25)
+ (message.channel as TextChannel).send("https://tenor.com/bOMAb.gif");
+ else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍");
+}
diff --git a/src/events/easterEggs/crazy.ts b/src/events/easterEggs/crazy.ts
new file mode 100644
index 0000000..8ded6ec
--- /dev/null
+++ b/src/events/easterEggs/crazy.ts
@@ -0,0 +1,9 @@
+import type { Message, TextChannel } from "discord.js";
+
+export default async function run(message: Message) {
+ if (!message.content.toLowerCase().includes("crazy")) return;
+ if (Math.round(Math.random()) <= 0.5)
+ await (message.channel as TextChannel).send(
+ "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..."
+ );
+}
diff --git a/src/events/easterEggs/fan.ts b/src/events/easterEggs/fan.ts
new file mode 100644
index 0000000..4d44dc6
--- /dev/null
+++ b/src/events/easterEggs/fan.ts
@@ -0,0 +1,13 @@
+import type { Message, TextChannel } from "discord.js";
+import { randomise } from "../../utils/randomise";
+
+export default async function run(message: Message) {
+ if (message.content.toLowerCase() != "i'm a big fan") return;
+ const gifs = randomise([
+ "https://tenor.com/bC37i.gif",
+ "https://tenor.com/view/fan-gif-20757784",
+ "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715"
+ ]);
+
+ await (message.channel as TextChannel).send(gifs);
+}
diff --git a/src/events/easterEggs/fire.ts b/src/events/easterEggs/fire.ts
new file mode 100644
index 0000000..2a728fc
--- /dev/null
+++ b/src/events/easterEggs/fire.ts
@@ -0,0 +1,14 @@
+import type { Message, TextChannel } from "discord.js";
+import { randomise } from "../../utils/randomise";
+
+export default async function run(message: Message) {
+ if (message.content.toLowerCase() != "fire in the hole") return;
+ const gifs = randomise([
+ "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&",
+ "https://tenor.com/view/dancing-gif-25178472",
+ "https://tenor.com/view/fire-in-the-hole-gif-11283103876805231056",
+ "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291"
+ ]);
+
+ await (message.channel as TextChannel).send(gifs);
+}
diff --git a/src/events/easterEggs/fireship.ts b/src/events/easterEggs/fireship.ts
new file mode 100644
index 0000000..9124f99
--- /dev/null
+++ b/src/events/easterEggs/fireship.ts
@@ -0,0 +1,13 @@
+import type { Message, TextChannel } from "discord.js";
+
+export default async function run(message: Message) {
+ const content = message.content.toLowerCase();
+ if (
+ content.startsWith("this has been") &&
+ content.endsWith("in 100 seconds") &&
+ message.content != "this has been in 100 seconds"
+ )
+ await (message.channel as TextChannel).send(
+ "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one"
+ );
+}
diff --git a/src/events/easterEggs/honk.ts b/src/events/easterEggs/honk.ts
new file mode 100644
index 0000000..9143205
--- /dev/null
+++ b/src/events/easterEggs/honk.ts
@@ -0,0 +1,7 @@
+import type { Message, TextChannel } from "discord.js";
+
+export default async function run(message: Message) {
+ const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"];
+ if (!honks.includes(message.content.toLowerCase())) return;
+ (message.channel as TextChannel).send("https://tenor.com/bW8sm.gif");
+}
diff --git a/src/events/easterEggs/whoPinged.ts b/src/events/easterEggs/whoPinged.ts
new file mode 100644
index 0000000..cdd1eb7
--- /dev/null
+++ b/src/events/easterEggs/whoPinged.ts
@@ -0,0 +1,18 @@
+import type { Message, TextChannel } from "discord.js";
+import { randomise } from "../../utils/randomise";
+
+export default async function run(message: Message) {
+ if (message.content.toLowerCase() != `<@${message.client.user.id}>`) return;
+ const gifs = randomise([
+ "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356",
+ "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226",
+ "https://tenor.com/view/who-pinged-me-ping-discord-discord-ping-undertaker-gif-20399650",
+ "https://tenor.com/view/who-pinged-me-ping-tudou-mr-potato-cat-gif-22762448",
+ "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322",
+ "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406",
+ "https://tenor.com/view/tense-table-smash-mad-gif-13656077",
+ "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175"
+ ]);
+
+ await (message.channel as TextChannel).send(gifs);
+}
diff --git a/src/events/error.ts b/src/events/error.ts
deleted file mode 100644
index b593e5b..0000000
--- a/src/events/error.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { type Client } from "discord.js";
-
-export default {
- name: "error",
- event: class Error {
- client: Client;
-
- constructor(client: Client) {
- this.client = client;
- }
-
- async run(err) {
- console.error(err);
- }
- }
-}
diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts
index 4b707a9..dd2242d 100644
--- a/src/events/guildCreate.ts
+++ b/src/events/guildCreate.ts
@@ -1,36 +1,34 @@
-import { EmbedBuilder, type Client, type Guild, DMChannel } from "discord.js";
-import Commands from "../handlers/commands.js";
-import randomise from "../utils/randomise.js";
-import { genColor } from "../utils/colorGen.js";
+import { EmbedBuilder, type DMChannel } from "discord.js";
+import { Commands } from "../handlers/commands";
+import { genColor } from "../utils/colorGen";
+import { randomise } from "../utils/randomise";
+import { Event } from "../utils/types";
-export default {
- name: "guildCreate",
- event: class GuildCreate {
- client: Client;
+export default (async function run(guild) {
+ const dmChannel = (await (await guild.fetchOwner()).createDM().catch(() => null)) as
+ | DMChannel
+ | undefined;
- constructor(client: Client) {
- this.client = client;
- }
+ let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"];
+ if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"];
- async run(guild: Guild) {
- const owner = await guild.fetchOwner();
- const dmChannel = (await owner.createDM().catch(() => null)) as DMChannel | null;;
- const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"];
+ const client = guild.client;
+ const embed = new EmbedBuilder()
+ .setAuthor({
+ name: `Welcome to ${client.user.username}!`,
+ iconURL: client.user.displayAvatarURL()
+ })
+ .setDescription(
+ [
+ "Sokora is a multipurpose Discord bot that lets you manage your servers easily.",
+ "To manage the bot, use the **/settings** command.\n",
+ "Sokora is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them."
+ ].join("\n")
+ )
+ .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` })
+ .setThumbnail(client.user.displayAvatarURL())
+ .setColor(genColor(200));
- const embed = new EmbedBuilder()
- .setTitle("👋 • Welcome to Nebula!")
- .setDescription([
- "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.",
- "To do things like disabling/enabling commands, use the **/settings** command.",
- "As of now, it's in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) to report them."
- ].join("\n\n"))
- .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` })
- .setColor(genColor(200));
-
- if (dmChannel) await dmChannel.send({ embeds: [embed] });
-
- const commands = new Commands(guild.client);
- await commands.registerCommandsForGuild(guild);
- }
- }
-}
+ await new Commands(client).registerCommandsForGuild(guild);
+ if (dmChannel) await dmChannel.send({ embeds: [embed] });
+} as Event<"guildCreate">);
diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts
new file mode 100644
index 0000000..0d2ef0c
--- /dev/null
+++ b/src/events/guildMemberAdd.ts
@@ -0,0 +1,35 @@
+import { EmbedBuilder, type TextChannel } from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { getSetting } from "../utils/database/settings";
+import { imageColor } from "../utils/imageColor";
+import { replace } from "../utils/replace";
+import { Event } from "../utils/types";
+
+export default (async function run(member) {
+ const guildID = member.guild.id;
+ const id = getSetting(guildID, "welcome", "channel") as string;
+ const user = member.user;
+ const avatar = member.displayAvatarURL();
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• ${user.displayName} has joined`, iconURL: avatar })
+ .setFooter({ text: `User ID: ${member.id}` })
+ .setThumbnail(avatar)
+ .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, avatar)) ?? genColor(200));
+
+ if (id) {
+ const channel = (await member.guild.channels.cache
+ .find(channel => channel.id == id)
+ ?.fetch()) as TextChannel;
+
+ replace(member, getSetting(guildID, "welcome", "join_text") as string, embed);
+ await channel.send({ embeds: [embed] });
+ }
+
+ if (!getSetting(guildID, "welcome", "join_dm") as boolean) return;
+ const dmChannel = await user.createDM().catch(() => null);
+ if (!dmChannel) return;
+ if (user.bot) return;
+
+ replace(member, getSetting(guildID, "welcome", "dm_text") as string, embed);
+ await dmChannel.send({ embeds: [embed] }).catch(() => null);
+} as Event<"guildMemberAdd">);
diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts
new file mode 100644
index 0000000..784f41d
--- /dev/null
+++ b/src/events/guildMemberRemove.ts
@@ -0,0 +1,26 @@
+import { EmbedBuilder, type GuildMember, type TextChannel } from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { getSetting } from "../utils/database/settings";
+import { imageColor } from "../utils/imageColor";
+import { replace } from "../utils/replace";
+import { Event } from "../utils/types";
+
+export default (async function run(member: GuildMember) {
+ const guildID = member.guild.id;
+ const id = getSetting(guildID, "welcome", "channel") as string;
+ if (!id) return;
+
+ const channel = (await member.guild.channels.cache
+ .find(channel => channel.id == id)
+ ?.fetch()) as TextChannel;
+
+ const avatar = member.displayAvatarURL();
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• ${member.user.displayName} has left`, iconURL: avatar })
+ .setFooter({ text: `User ID: ${member.id}` })
+ .setThumbnail(avatar)
+ .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, avatar)) ?? genColor(200));
+
+ replace(member, getSetting(guildID, "welcome", "leave_text") as string, embed);
+ await channel.send({ embeds: [embed] });
+} as Event<"guildMemberRemove">);
diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts
new file mode 100644
index 0000000..ce203ca
--- /dev/null
+++ b/src/events/interactionCreate.ts
@@ -0,0 +1,44 @@
+import { file } from "bun";
+import type { AutocompleteInteraction, CommandInteraction } from "discord.js";
+import { join } from "path";
+import { pathToFileURL } from "url";
+import { capitalize } from "../utils/capitalize";
+import { Event } from "../utils/types";
+
+async function getCommand(
+ interaction: CommandInteraction | AutocompleteInteraction,
+ options: any
+): Promise {
+ const commandName = capitalize(interaction.commandName)!;
+ const subcommandName = capitalize(options.getSubcommand(false));
+ const commandGroupName = capitalize(options.getSubcommandGroup(false));
+ let commandImportPath = join(
+ join(process.cwd(), "src", "commands"),
+ `${
+ subcommandName
+ ? `${commandName.toLowerCase()}/${
+ commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName
+ }`
+ : commandName
+ }.ts`
+ );
+
+ if (!(await file(commandImportPath).exists()))
+ commandImportPath = join(join(process.cwd(), "src", "commands", `${commandName}.ts`));
+
+ return new (await import(pathToFileURL(commandImportPath).toString())).default();
+}
+
+export default (async function run(interaction) {
+ if (interaction.isChatInputCommand()) {
+ const command = await getCommand(interaction, interaction.options);
+ if (!command) return;
+ if (command.deferred) await interaction.deferReply();
+ command.run(interaction);
+ } else if (interaction.isAutocomplete()) {
+ const command = await getCommand(interaction, interaction.options);
+ if (!command) return;
+ if (!command.autocomplete) return;
+ command.autocomplete(interaction);
+ }
+} as Event<"interactionCreate">);
diff --git a/src/events/leveling.ts b/src/events/leveling.ts
deleted file mode 100644
index 54eccac..0000000
--- a/src/events/leveling.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import { ColorResolvable, EmbedBuilder, TextChannel, type Message } from "discord.js";
-import Vibrant from "node-vibrant";
-import sharp from "sharp";
-import { genColor, genRGBColor } from "../utils/colorGen.js";
-import database, { getLevelingTable, getSettingsTable } from "../utils/database.js";
-import { Reward } from "../commands/settings/leveling/rewards.js";
-
-export const EXP_PER_MESSAGE = 10; // 10 exp per message
-export const BASE_EXP_FOR_NEW_LEVEL = 10 * 100; // 1000 messages to level up
-export const DIFFICULTY_MULTIPLIER = 1.25; // 1.25x harder to level up each level
-
-export default {
- name: "messageCreate",
- event: class MessageCreate {
- db = null;
-
- async run(message: Message) {
- if (!this.db) this.db = await database();
- const db = this.db;
- const levelingTable = await getLevelingTable(db);
- const settingsTable = await getSettingsTable(db);
- const target = message.author;
-
- if (message.author.bot) return;
-
- const levelingEnabled = await settingsTable?.get(`${message.guild.id}.leveling.enabled`).then(
- (enabled) => !!enabled
- ).catch(() => false);
-
- if (!levelingEnabled) return;
-
- const { exp, levels } = await levelingTable?.get(`${message.guild.id}.${target.id}`).catch(() => {
- return {
- exp: 0,
- levels: 0
- };
- });
- const { exp: expGlobal, levels: levelsGlobal } = await levelingTable?.get(`global.${target.id}`).then(
- (data) => {
- if (!data) return {
- exp: 0,
- levels: 0
- };
- return {
- exp: Number(data.exp),
- levels: Number(data.levels)
- };
- }
- ).catch(() => {
- return {
- exp: 0,
- levels: 0
- };
- });
-
- const expUntilLevelup = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levels + 1));
- const expUntilLevelupGlobal = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levelsGlobal + 1));
- const expUntilNextLevelup = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levels + 2));
-
- const newLevelData = {
- levels: levels ?? 0,
- exp: (exp ?? 0) + EXP_PER_MESSAGE
- };
-
- const newLevelDataGlobal = {
- levels: levelsGlobal ?? 0,
- exp: (expGlobal ?? 0) + EXP_PER_MESSAGE
- };
-
- if (!(exp >= expUntilLevelup - 1)) {
- await levelingTable.set(`global.${target.id}`, newLevelDataGlobal);
- return await levelingTable.set(`${message.guild.id}.${target.id}`, newLevelDataGlobal);
- } else if (exp >= expUntilLevelup - 1) {
- let leftOverExp = exp - expUntilLevelup;
- if (leftOverExp < 0) leftOverExp = 0;
-
- newLevelData.levels = levels + 1;
- newLevelData.exp = leftOverExp ?? 0;
-
- await levelingTable.set(`${message.guild.id}.${target.id}`, newLevelData);
- }
-
- if (exp >= expUntilLevelupGlobal - 1) {
- let leftOverExpGlobal = exp - expUntilLevelup;
- if (leftOverExpGlobal < 0) leftOverExpGlobal = 0;
-
- newLevelDataGlobal.levels = levels + 1;
- newLevelDataGlobal.exp = leftOverExpGlobal + 1;
-
- await levelingTable.set(`global.${target.id}`, newLevelDataGlobal);
- }
-
- // Check if there's a level up channel
- const levelChannelId = await settingsTable?.get(`${message.guild.id}.leveling.channel`).then(
- (channelId) => String(channelId)
- ).catch(() => null);
- if (!levelChannelId) return;
- const levelChannel = message.guild.channels.cache.get(levelChannelId) as TextChannel
-
- // Level up embed
- const leveledEmbed = new EmbedBuilder()
- .setTitle("⚡ • Level Up!")
- .setDescription([
- `**Congratulations <@${target.id}>**!`,
- `You made it to **level ${levels + 1}**`,
- `You need ${expUntilNextLevelup} exp to level up again.`
- ].join("\n"))
- .setAuthor({
- name: target.displayName,
- url: target.avatarURL()
- })
- .setThumbnail(target.avatarURL())
- .setColor(genColor(200))
- .setTimestamp();
-
- // Get vibrant color
- try {
- const imageBuffer = await (await fetch(target.avatarURL())).arrayBuffer();
- const image = sharp(imageBuffer).toFormat("jpg");
- const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant;
- leveledEmbed.setColor(genRGBColor(r, g, b) as ColorResolvable);
- } catch { }
-
- // Sending the level up
- levelChannel.send({
- embeds: [leveledEmbed],
- content: `<@${target.id}>`
- });
-
- // Checking if there is a level reward
- const levelRewards = await settingsTable?.get(`${message.guild.id}.leveling.rewards`).then(
- (rewards) => rewards as Reward[] ?? [] as Reward[]
- ).catch(() => [] as Reward[]);
- const members = await message.guild.members.fetch();
- for (const { level, roleId } of levelRewards) {
- const role = message.guild.roles.cache.get(roleId);
-
- if (levels >= level) {
- await members.get(target.id)?.roles.add(role);
- continue;
- }
-
- await members.get(target.id)?.roles.remove(role);
- }
- }
- }
-}
diff --git a/src/events/loadCommands.ts b/src/events/loadCommands.ts
deleted file mode 100644
index 2530122..0000000
--- a/src/events/loadCommands.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import type { CommandInteraction, Interaction, Client, AutocompleteInteraction } from "discord.js";
-import { pathToFileURL } from "url";
-import { join } from "path";
-import database from "../utils/database.js";
-import { QuickDB } from "quick.db";
-
-const COMMANDS_PATH = join(process.cwd(), "src", "commands");
-
-async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any, db: QuickDB): Promise {
- const commandName = interaction.commandName;
- const subcommandName = options.getSubcommand(false);
- const commandgroupName = options.getSubcommandGroup(false);
-
- const commandImportPath = join(
- COMMANDS_PATH,
- `${subcommandName ?
- `${commandName}/${commandgroupName ?
- `${commandgroupName}/${subcommandName}` :
- subcommandName}` :
- commandName}.ts`
- );
- const command = new (await import(pathToFileURL(commandImportPath).toString())).default(db);
- return command;
-}
-
-export default {
- name: "interactionCreate",
- event: class InteractionCreate {
- commands: CommandInteraction;
- client: Client;
- lastOpenedDb = Date.now();
- db: QuickDB = null;
-
- constructor(cmds: CommandInteraction, client: Client) {
- this.commands = cmds;
- this.client = client;
- this.lastOpenedDb = Date.now();
- }
-
- async run(interaction: Interaction) {
- if (!this.db) this.db = await database();
- if (Date.now() - this.lastOpenedDb > 1000 * 60 * 60) {
- this.db = await database();
- this.lastOpenedDb = Date.now();
- }
-
- if (interaction.isChatInputCommand()) {
- const options = interaction.options
-
- // Command names
- const command = await getCommand(interaction, options, this.db).catch(() => null);
- if (!command) return;
-
- const deferred = command?.deferred ?? true;
- if (deferred) await interaction.deferReply();
-
- command.run(interaction);
- } else if (interaction.isAutocomplete()) {
- const options = interaction.options;
-
- const command = await getCommand(interaction, options, this.db).catch(() => null);
- if (!command) return;
- if (!command.autocomplete) return;
- command.autocomplete(interaction);
- }
- }
- }
-};
diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts
new file mode 100644
index 0000000..333e5dc
--- /dev/null
+++ b/src/events/messageCreate.ts
@@ -0,0 +1,82 @@
+import { EmbedBuilder, type TextChannel } from "discord.js";
+import { readdirSync } from "fs";
+import { join } from "path";
+import { pathToFileURL } from "url";
+import { genColor } from "../utils/colorGen";
+import { getLevel, setLevel } from "../utils/database/leveling";
+import { getSetting } from "../utils/database/settings";
+import { kominator } from "../utils/kominator";
+import { Event } from "../utils/types";
+
+const cooldowns = new Map();
+export default (async function run(message) {
+ const author = message.author;
+ if (author.bot) return;
+ const guild = message.guild!;
+
+ // Easter egg handler
+ if (getSetting(guild.id, "easter", "enabled")) {
+ const eventsPath = join(process.cwd(), "src", "events", "easterEggs");
+
+ for (const easterEggFile of readdirSync(eventsPath))
+ (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default(message);
+ }
+
+ // Leveling
+ if (!getSetting(guild.id, "leveling", "enabled")) return;
+
+ const blockedChannels = getSetting(guild.id, "leveling", "block_channels") as string;
+ if (blockedChannels != undefined)
+ for (const channelID of kominator(blockedChannels)) if (message.channelId == channelID) return;
+
+ const cooldown = getSetting(guild.id, "leveling", "cooldown") as number;
+ if (cooldown > 0) {
+ const key = `${guild.id}-${author.id}`;
+ const lastExpTime = cooldowns.get(key) || 0;
+ const now = Date.now();
+
+ if (now - lastExpTime < cooldown * 1000) return;
+ else cooldowns.set(key, now);
+ }
+
+ const xpGain = getSetting(guild.id, "leveling", "xp_gain") as number;
+ const levelChannelId = getSetting(guild.id, "leveling", "channel");
+ const difficulty = getSetting(guild.id, "leveling", "difficulty") as number;
+ const [level, xp] = getLevel(guild.id, author.id);
+ const xpUntilLevelUp = Math.floor(
+ 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2
+ );
+ const newLevelData = { level: level ?? 0, xp: xp + xpGain };
+
+ if (newLevelData.xp < xpUntilLevelUp)
+ return setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp);
+
+ while (
+ newLevelData.xp >=
+ 100 * difficulty * (newLevelData.level + 1) ** 2 - 85 * difficulty * newLevelData.level ** 2
+ )
+ newLevelData.level++;
+
+ setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp);
+ const embed = new EmbedBuilder()
+ .setAuthor({
+ name: `• ${author.displayName} has levelled up!`,
+ iconURL: author.displayAvatarURL()
+ })
+ .setDescription(
+ [
+ `**Congratulations, ${author.displayName}**!`,
+ `You made it to **level ${level + 1}**`,
+ `You need ${Math.floor(100 * difficulty * (level + 2))} XP to level up again.`
+ ].join("\n")
+ )
+ .setThumbnail(author.displayAvatarURL())
+ .setTimestamp()
+ .setColor(genColor(200));
+
+ if (levelChannelId)
+ (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({
+ embeds: [embed],
+ content: `<@${author.id}>`
+ });
+} as Event<"messageCreate">);
diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts
new file mode 100644
index 0000000..3f2d05f
--- /dev/null
+++ b/src/events/messageDelete.ts
@@ -0,0 +1,27 @@
+import { codeBlock, EmbedBuilder } from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { getSetting } from "../utils/database/settings";
+import { logChannel } from "../utils/logChannel";
+import { Event } from "../utils/types";
+
+export default (async function run(message) {
+ const author = message.author!;
+ if (author.bot) return;
+
+ const guild = message.guild!;
+ if (!getSetting(guild.id, "moderation", "log_messages")) return;
+
+ const embed = new EmbedBuilder()
+ .setAuthor({
+ name: `• ${author.displayName}'s message has been deleted.`,
+ iconURL: author.displayAvatarURL()
+ })
+ .addFields({
+ name: "🗞️ • Deleted message",
+ value: codeBlock(message.content!)
+ })
+ .setFooter({ text: `Message ID: ${message.id}\nUser ID: ${author.id}` })
+ .setColor(genColor(60));
+
+ await logChannel(guild, embed);
+} as Event<"messageDelete">);
diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts
new file mode 100644
index 0000000..f76ff05
--- /dev/null
+++ b/src/events/messageUpdate.ts
@@ -0,0 +1,37 @@
+import { codeBlock, EmbedBuilder } from "discord.js";
+import { genColor } from "../utils/colorGen";
+import { getSetting } from "../utils/database/settings";
+import { logChannel } from "../utils/logChannel";
+import { Event } from "../utils/types";
+
+export default (async function run(oldMessage, newMessage) {
+ const author = oldMessage.author!;
+ if (author.bot) return;
+
+ const guild = oldMessage.guild!;
+ if (!getSetting(guild.id, "moderation", "log_messages")) return;
+
+ const oldContent = oldMessage.content!;
+ const newContent = newMessage.content!;
+ if (oldContent == newContent) return;
+
+ const embed = new EmbedBuilder()
+ .setAuthor({
+ name: `• ${author.displayName}'s message has been edited`,
+ iconURL: author.displayAvatarURL()
+ })
+ .addFields(
+ {
+ name: "🕰️ • Old message",
+ value: codeBlock(oldContent)
+ },
+ {
+ name: "🔄️ • New message",
+ value: codeBlock(newContent)
+ }
+ )
+ .setFooter({ text: `Message ID: ${oldMessage.id}\nUser ID: ${author.id}` })
+ .setColor(genColor(60));
+
+ await logChannel(guild, embed);
+} as Event<"messageUpdate">);
diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts
index 6e1e78e..792dbb3 100644
--- a/src/handlers/commands.ts
+++ b/src/handlers/commands.ts
@@ -1,43 +1,49 @@
-import { SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, Guild, type Client } from "discord.js";
-import { pathToFileURL } from "url";
-import { join } from "path";
+import {
+ Guild,
+ SlashCommandBuilder,
+ SlashCommandSubcommandGroupBuilder,
+ type Client
+} from "discord.js";
import { readdirSync } from "fs";
-import database, { getSettingsTable } from "../utils/database.js";
-import { QuickDB } from "quick.db";
+import { join } from "path";
+import { pathToFileURL } from "url";
+import { getDisabledCommands } from "../utils/database/disabledCommands";
-const COMMANDS_PATH = join(process.cwd(), "src", "commands");
-export default class Commands {
+export class Commands {
client: Client;
commands: any[] = [];
- db: QuickDB;
-
- constructor(client?: Client) {
+ constructor(client: Client) {
this.client = client;
}
- // Load the commands into this.commands
- private async createSubCommand(name: string, ...disabledCommands: string[]): Promise {
+ private async createSubCommand(
+ name: string,
+ ...disabledCommands: string[]
+ ): Promise {
+ const commandsPath = join(process.cwd(), "src", "commands");
const command = new SlashCommandBuilder()
.setName(name.toLowerCase())
.setDescription("This command has no description.");
- // Fetch the subcommands
- const subCommandFiles = readdirSync(join(COMMANDS_PATH, name), { withFileTypes: true });
-
- // Add the subcommands to the top command
- for (const subCommandFile of subCommandFiles) {
+ for (const subCommandFile of readdirSync(join(commandsPath, name), {
+ withFileTypes: true
+ })) {
const subCommandName = subCommandFile.name.replaceAll(".ts", "");
if (
disabledCommands?.find(
- (command) => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName
+ command => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName
)
- ) continue;
+ )
+ continue;
if (subCommandFile.isFile()) {
- const subCommand = await import(pathToFileURL(join(COMMANDS_PATH, name, subCommandFile.name)).toString());
- try {
- command.addSubcommand(new subCommand.default().data);
- } catch {}
+ const subCommandModule = await import(
+ pathToFileURL(join(commandsPath, name, subCommandFile.name)).toString()
+ );
+ const subCommand = new subCommandModule.default();
+
+ command.addSubcommand(subCommand.data);
+ if ("autocompleteHandler" in subCommand) subCommand.autocompleteHandler(this.client);
continue;
}
@@ -45,21 +51,25 @@ export default class Commands {
.setName(subCommandName.toLowerCase())
.setDescription("This subcommand group has no description.");
- const subCommandGroupFiles = readdirSync(join(COMMANDS_PATH, name, subCommandFile.name), { withFileTypes: true });
-
+ const subCommandGroupFiles = readdirSync(join(commandsPath, name, subCommandFile.name), {
+ withFileTypes: true
+ });
for (const subCommandGroupFile of subCommandGroupFiles) {
if (!subCommandGroupFile.isFile()) continue;
if (
disabledCommands?.find(
- (command) =>
+ command =>
command?.split("/")?.[0] == name &&
command?.split("/")?.[1] == subCommandFile.name.replaceAll(".ts", "") &&
command?.split("/")?.[2] == subCommandGroupFile.name.replaceAll(".ts", "")
)
- ) continue;
+ )
+ continue;
const subCommand = await import(
- pathToFileURL(join(COMMANDS_PATH, name, subCommandFile.name, subCommandGroupFile.name)).toString()
+ pathToFileURL(
+ join(commandsPath, name, subCommandFile.name, subCommandGroupFile.name)
+ ).toString()
);
subCommandGroup.addSubcommand(new subCommand.default().data);
}
@@ -70,46 +80,40 @@ export default class Commands {
}
async loadCommands(...disabledCommands: string[]) {
+ const commandsPath = join(process.cwd(), "src", "commands");
this.commands = [];
- const commandFiles = readdirSync(COMMANDS_PATH, { withFileTypes: true });
+ const commandFiles = readdirSync(commandsPath, { withFileTypes: true });
for (const commandFile of commandFiles) {
const name = commandFile.name;
if (disabledCommands?.includes(name.replaceAll(".ts", ""))) continue;
if (commandFile.isFile()) {
- // Add the commands it found to the list
- const command = await import(pathToFileURL(join(COMMANDS_PATH, name)).toString());
- this.commands.push(new command.default().data);
+ const commandImport = await import(pathToFileURL(join(commandsPath, name)).toString());
+ this.commands.push(new commandImport.default().data);
continue;
}
- // Folder found -> Make subcommand
- // Create a top subcommand
- const subCommand = await this.createSubCommand(name, join(COMMANDS_PATH, name), ...disabledCommands);
+ const subCommand = await this.createSubCommand(
+ name,
+ join(commandsPath, name),
+ ...disabledCommands
+ );
this.commands.push(subCommand);
}
}
- // Register the commands for a specific guild
async registerCommandsForGuild(guild: Guild, ...disabledCommands: string[]) {
await this.loadCommands(...disabledCommands);
await guild.commands.set(this.commands);
}
- // Register the commands for all guilds
async registerCommands(): Promise {
- const db = await database();
await this.loadCommands();
- const settingsTable = await getSettingsTable(db);
const guilds = this.client.guilds.cache;
- // Adding the commands to the guilds
- console.log("Adding commands to guilds...");
for (const guildID of guilds.keys()) {
- const disabledCommands = await settingsTable?.get(`${guildID}.disabledCommands`).then(
- (disabledCommands: string[]) => disabledCommands as string[] ?? [] as string[]
- ).catch(() => [] as string[]);
+ const disabledCommands = getDisabledCommands(guildID);
if (disabledCommands.length > 0) await this.loadCommands(...disabledCommands);
await guilds.get(guildID)?.commands.set(this.commands);
}
diff --git a/src/handlers/events.ts b/src/handlers/events.ts
index c7964f9..3ac1b39 100644
--- a/src/handlers/events.ts
+++ b/src/handlers/events.ts
@@ -1,26 +1,26 @@
import type { Client } from "discord.js";
-import { pathToFileURL } from "url";
-import { join } from "path";
import { readdirSync } from "fs";
+import { join } from "path";
+import { pathToFileURL } from "url";
-export default class Events {
+export class Events {
client: Client;
events: any[] = [];
-
constructor(client: Client) {
this.client = client;
+ }
- (async () => {
- const eventsPath = join(process.cwd(), "src", "events");
+ async loadEvents() {
+ const eventsPath = join(process.cwd(), "src", "events");
- for (const eventFile of readdirSync(eventsPath)) {
- if (!eventFile.endsWith("js")) continue;
+ for (const eventFile of readdirSync(eventsPath)) {
+ if (!eventFile.endsWith("ts")) continue;
- const event = await import(pathToFileURL(join(eventsPath, eventFile)).toString());
- const clientEvent = this.client.on(event.default.name, new event.default.event(this.client).run);
+ const event = (await import(pathToFileURL(join(eventsPath, eventFile)).toString())).default;
+ const eventName = eventFile.split(".ts")[0];
+ const clientEvent = this.client.on(eventName, event);
- this.events.push({ name: event.default.name, event: clientEvent });
- }
- })();
+ this.events.push({ name: eventName, event: clientEvent });
+ }
}
}
diff --git a/src/index.ts b/src/index.ts
index 969a9d9..e4be99a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,6 @@
import { ShardingManager } from "discord.js";
const manager = new ShardingManager("./src/bot.ts", { token: process.env.TOKEN });
-
manager.on("shardCreate", shard => {
shard.on("error", err => console.error(err));
console.log(`Launched shard ${shard.id}`);
diff --git a/src/utils/capitalize.ts b/src/utils/capitalize.ts
new file mode 100644
index 0000000..9c2d7cf
--- /dev/null
+++ b/src/utils/capitalize.ts
@@ -0,0 +1,9 @@
+/**
+ * Outputs the string with the first letter capitalized.
+ * @param string String, the first letter of which should be capitalized.
+ */
+
+export function capitalize(string: string) {
+ if (!string) return;
+ return `${string.charAt(0).toUpperCase()}${string.slice(1)}`;
+}
diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts
index 018a46d..c00e6b0 100644
--- a/src/utils/colorGen.ts
+++ b/src/utils/colorGen.ts
@@ -28,14 +28,15 @@ export function genColor(hue: number) {
* @param b Blue.
* @returns Color in HEX.
*/
-export function genRGBColor(r, g, b) {
+
+export function genRGBColor(r: any, g: any, b: any) {
r = r.toString(16);
g = g.toString(16);
b = b.toString(16);
- if (r.length === 1) r = "0" + r;
- if (g.length === 1) g = "0" + g;
- if (b.length === 1) b = "0" + b;
+ if (r.length == 1) r = `0${r}`;
+ if (g.length == 1) g = `0${g}`;
+ if (b.length == 1) b = `0${b}`;
return `#${r}${g}${b}`;
}
diff --git a/src/utils/database.ts b/src/utils/database.ts
deleted file mode 100644
index 52e5764..0000000
--- a/src/utils/database.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { QuickDB, MySQLDriver } from "quick.db";
-
-async function database() {
- const mysql = new MySQLDriver({
- host: process.env.MYSQL_HOST,
- user: process.env.MYSQL_USERNAME,
- port: Number(process.env.MYSQL_PORT),
- password: process.env.MYSQL_PASSWORD,
- database: process.env.MYSQL_DATABASE,
- enableKeepAlive: true,
- compress: true
- });
-
- await mysql.connect();
- const db = new QuickDB({ driver: mysql });
- return db;
-}
-
-export const getSettingsTable = async (db: QuickDB) => await db.tableAsync("settings");
-export const getLevelingTable = async (db: QuickDB) => await db.tableAsync("leveling");
-export const getServerboardTable = async (db: QuickDB) => await db.tableAsync("serverboard");
-export const getNewsTable = async (db: QuickDB) => await db.tableAsync("news");
-export const getModerationTable = async (db: QuickDB) => await db.tableAsync("moderation");
-
-export default database;
diff --git a/src/utils/database/disabledCommands.ts b/src/utils/database/disabledCommands.ts
new file mode 100644
index 0000000..d0e05dd
--- /dev/null
+++ b/src/utils/database/disabledCommands.ts
@@ -0,0 +1,31 @@
+import { getDatabase } from ".";
+import { TableDefinition, TypeOfDefinition } from "./types";
+
+const tableDefinition = {
+ name: "disabledCommands",
+ definition: {
+ guild: "TEXT",
+ command: "TEXT"
+ }
+} satisfies TableDefinition;
+
+const database = getDatabase(tableDefinition);
+const getQuery = database.query("SELECT * FROM disabledCommands WHERE guild = $1;");
+const addQuery = database.query("INSERT INTO disabledCommands (guild, command) VALUES (?1, ?2);");
+const removeQuery = database.query(
+ "DELETE FROM disabledCommands WHERE guild = $1 AND command = $2"
+);
+
+export function getDisabledCommands(guildID: string) {
+ return (getQuery.all(guildID) as TypeOfDefinition[]).map(
+ val => val.command
+ );
+}
+
+export function disableCommands(guildID: string, command: string) {
+ addQuery.run(guildID, command);
+}
+
+export function enableCommands(guildID: string, command: string) {
+ removeQuery.run(guildID, command);
+}
diff --git a/src/utils/database/index.ts b/src/utils/database/index.ts
new file mode 100644
index 0000000..27713ac
--- /dev/null
+++ b/src/utils/database/index.ts
@@ -0,0 +1,14 @@
+import { Database } from "bun:sqlite";
+import { TableDefinition } from "./types";
+
+// Get (or create) an SQLite database
+const database = new Database("data.db", { create: true });
+
+export function getDatabase(definition: TableDefinition) {
+ // Create table if it doesn't exist
+ const defStr = Object.entries(definition.definition)
+ .map(([field, type]) => field.concat(" ", type))
+ .join(", ");
+ database.run(`CREATE TABLE IF NOT EXISTS ${definition.name} (${defStr});`);
+ return database;
+}
diff --git a/src/utils/database/levelBlockedChannels.ts b/src/utils/database/levelBlockedChannels.ts
new file mode 100644
index 0000000..3bcf30a
--- /dev/null
+++ b/src/utils/database/levelBlockedChannels.ts
@@ -0,0 +1,40 @@
+import { getDatabase } from ".";
+import { TableDefinition, TypeOfDefinition } from "./types";
+
+const tableDefinition = {
+ name: "levelBlockedChannels",
+ definition: {
+ guild: "TEXT",
+ channel: "TEXT"
+ }
+} satisfies TableDefinition;
+
+const database = getDatabase(tableDefinition);
+const getQuery = database.query(
+ "SELECT * FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;"
+);
+const listQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1;");
+const addQuery = database.query(
+ "INSERT INTO levelBlockedChannels (guild, channel) VALUES (?1, ?2);"
+);
+const removeQuery = database.query(
+ "DELETE FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;"
+);
+
+export function getBlockedChannels(guildID: string, channelID: string) {
+ return getQuery.all(guildID, channelID).length == 0;
+}
+
+export function listBlockedChannels(guildID: string) {
+ return (listQuery.all(guildID) as TypeOfDefinition[]).map(
+ val => val.channel
+ );
+}
+
+export function blockChannel(guildID: string, channelID: string) {
+ addQuery.run(guildID, channelID);
+}
+
+export function unblockChannel(guildID: string, channelID: string) {
+ removeQuery.run(guildID, channelID);
+}
diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts
new file mode 100644
index 0000000..9255f80
--- /dev/null
+++ b/src/utils/database/levelRewards.ts
@@ -0,0 +1,36 @@
+// TODO: Implement logic
+import { getDatabase } from ".";
+import { TableDefinition, TypeOfDefinition } from "./types";
+
+const tableDefinition = {
+ name: "levelRewards",
+ definition: {
+ guild: "TEXT",
+ roleID: "TEXT",
+ level: "INTEGER"
+ }
+} satisfies TableDefinition;
+
+const database = getDatabase(tableDefinition);
+const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;");
+const addQuery = database.query(
+ "INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);"
+);
+const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND roleID = $2");
+
+export function get(guildID: string) {
+ return getQuery.all(guildID) as TypeOfDefinition[];
+}
+
+export function addReward(guildID: string, role: number | string, level: number) {
+ return addQuery.all(guildID, level, role) as TypeOfDefinition[];
+}
+
+export function updateReward(guildID: string, role: number | string, level: number) {
+ removeQuery.run(guildID, role);
+ addQuery.all(guildID, level, role);
+}
+
+export function removeReward(guildID: number | string, role: number | string) {
+ removeQuery.run(guildID, role);
+}
diff --git a/src/utils/database/leveling.ts b/src/utils/database/leveling.ts
new file mode 100644
index 0000000..1a23d53
--- /dev/null
+++ b/src/utils/database/leveling.ts
@@ -0,0 +1,37 @@
+import { getDatabase } from ".";
+import { TableDefinition, TypeOfDefinition } from "./types";
+
+const tableDefinition = {
+ name: "leveling",
+ definition: {
+ guild: "TEXT",
+ user: "TEXT",
+ level: "INTEGER",
+ xp: "INTEGER"
+ }
+} satisfies TableDefinition;
+
+const database = getDatabase(tableDefinition);
+
+const getQuery = database.query("SELECT * FROM leveling WHERE guild = $1 AND user = $2;");
+const deleteQuery = database.query("DELETE FROM leveling WHERE guild = $1 AND user = $2;");
+const insertQuery = database.query(
+ "INSERT INTO leveling (guild, user, level, xp) VALUES (?1, ?2, ?3, ?4);"
+);
+
+const getGuildQuery = database.query("SELECT * FROM leveling WHERE guild = $1;");
+
+export function getLevel(guildID: string, userID: string): [number, number] {
+ const res = getQuery.all(guildID, userID) as TypeOfDefinition[];
+ if (!res.length) return [0, 0];
+ return [res[0].level, res[0].xp];
+}
+
+export function setLevel(guildID: string | number, userID: string, level: number, xp: number) {
+ if (getQuery.all(guildID, userID).length) deleteQuery.run(guildID, userID);
+ insertQuery.run(guildID, userID, level, xp);
+}
+
+export function getGuildLeaderboard(guildID: string): TypeOfDefinition[] {
+ return getGuildQuery.all(guildID) as TypeOfDefinition[];
+}
diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts
new file mode 100644
index 0000000..ce3fc90
--- /dev/null
+++ b/src/utils/database/moderation.ts
@@ -0,0 +1,86 @@
+import { getDatabase } from ".";
+import { TableDefinition, TypeOfDefinition } from "./types";
+
+const definition = {
+ name: "moderation",
+ definition: {
+ guild: "TEXT",
+ user: "TEXT",
+ type: "TEXT",
+ moderator: "TEXT",
+ reason: "TEXT",
+ id: "TEXT",
+ timestamp: "TIMESTAMP",
+ expiresAt: "TIMESTAMP"
+ }
+} satisfies TableDefinition;
+
+export type modType = "MUTE" | "WARN" | "KICK" | "BAN";
+const database = getDatabase(definition);
+const addQuery = database.query(
+ "INSERT INTO moderation (guild, user, type, moderator, reason, id, timestamp, expiresAt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);"
+);
+const listGuildQuery = database.query("SELECT * FROM moderation WHERE guild = $1");
+const listUserQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND user = $2;");
+const listUserTypeQuery = database.query(
+ "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;"
+);
+const listModQuery = database.query(
+ "SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;"
+);
+const getIdQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND id = $2;");
+const removeQuery = database.query("DELETE FROM moderation WHERE guild = $1 AND id = $2");
+
+export function addModeration(
+ guildID: string | number,
+ userID: string,
+ type: modType,
+ moderator: string,
+ reason = "",
+ expiresAt?: number | null
+) {
+ const id = listGuildQuery.all(guildID).length + 1;
+ addQuery.run(guildID, userID, type, moderator, reason, id, Date.now(), expiresAt ?? null);
+ return id;
+}
+
+export function listUserModeration(
+ guildID: number | string,
+ userID: number | string,
+ type?: modType
+) {
+ if (type)
+ return listUserTypeQuery.all(guildID, userID, type) as TypeOfDefinition[];
+
+ return listUserQuery.all(guildID, userID) as TypeOfDefinition[];
+}
+
+export function getModeration(guildID: number | string, userID: number | string, id: string) {
+ const modCase = getIdQuery.all(guildID, id) as TypeOfDefinition[];
+ if (modCase[0].user == userID) return modCase;
+ return [];
+}
+
+export function listModeratorLog(guildID: number | string, moderator: number | string) {
+ return listModQuery.all(guildID, moderator) as TypeOfDefinition[];
+}
+
+export function removeModeration(guildID: string | number, id: string) {
+ removeQuery.run(guildID, id);
+}
+
+const getExpiredBansQuery = database.query(
+ "SELECT * FROM moderation WHERE type = 'BAN' AND expiresAt IS NOT NULL AND expiresAt <= $1;"
+);
+
+export function getExpiredBans(currentTime: number) {
+ return getExpiredBansQuery.all(currentTime) as TypeOfDefinition[];
+}
+
+const getPendingBansQuery = database.query(
+ "SELECT * FROM moderation WHERE type = 'BAN' AND expiresAt IS NOT NULL AND expiresAt > $1;"
+);
+
+export function getPendingBans(currentTime: number) {
+ return getPendingBansQuery.all(currentTime) as TypeOfDefinition[];
+}
diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts
new file mode 100644
index 0000000..bfff50d
--- /dev/null
+++ b/src/utils/database/news.ts
@@ -0,0 +1,65 @@
+import { getDatabase } from ".";
+import { TableDefinition, TypeOfDefinition } from "./types";
+
+const definition = {
+ name: "news",
+ definition: {
+ guildID: "TEXT",
+ title: "TEXT",
+ body: "TEXT",
+ author: "TEXT",
+ authorPFP: "TEXT",
+ createdAt: "TIMESTAMP",
+ updatedAt: "TIMESTAMP",
+ messageID: "TEXT",
+ id: "TEXT"
+ }
+} satisfies TableDefinition;
+
+const database = getDatabase(definition);
+const sendQuery = database.query(
+ "INSERT INTO news (guildID, title, body, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);"
+);
+export const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;");
+const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;");
+const deleteQuery = database.query("DELETE FROM news WHERE id = $1");
+
+export function addNews(
+ guildID: string,
+ title: string,
+ body: string,
+ author: string,
+ authorPFP: string,
+ messageID: string,
+ id: string
+) {
+ sendQuery.run(guildID, title, body, author, authorPFP, Date.now(), 0, messageID, id);
+}
+
+export function listAllNews(guildID: string) {
+ return listAllQuery.all(guildID) as TypeOfDefinition[];
+}
+
+export function get(id: string) {
+ return getIdQuery.get(id) as TypeOfDefinition | null;
+}
+
+export function updateNews(id: string, title?: string, body?: string, messageID?: string) {
+ const lastElem = get(id)!;
+ deleteQuery.run(id);
+ sendQuery.run(
+ lastElem.guildID,
+ title ?? lastElem.title,
+ body ?? lastElem.body,
+ lastElem.author,
+ lastElem.authorPFP,
+ Date.now(),
+ 0,
+ messageID ?? lastElem.messageID,
+ id
+ );
+}
+
+export function deleteNews(id: string) {
+ deleteQuery.run(id);
+}
diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts
new file mode 100644
index 0000000..d5679f6
--- /dev/null
+++ b/src/utils/database/settings.ts
@@ -0,0 +1,197 @@
+import { getDatabase } from ".";
+import { kominator } from "../kominator";
+import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types";
+
+const tableDefinition = {
+ name: "settings",
+ definition: {
+ guildID: "TEXT",
+ key: "TEXT",
+ value: "TEXT"
+ }
+} satisfies TableDefinition;
+
+export const settingsDefinition: Record<
+ string,
+ {
+ description: string;
+ settings: Record;
+ }
+> = {
+ leveling: {
+ description: "Customise the behaviour of the leveling system.",
+ settings: {
+ enabled: {
+ type: "BOOL",
+ desc: "Enable/disable the leveling system.",
+ val: true
+ },
+ channel: {
+ type: "TEXT",
+ desc: "ID of the log channel for leveling-related stuff (i.e someone leveling up)."
+ },
+ block_channels: {
+ type: "TEXT",
+ desc: "ID(s) of the channels where messages aren't counted, comma separated."
+ },
+ xp_gain: {
+ type: "INTEGER",
+ desc: "Set the amount of XP a user gains per message.",
+ val: 5
+ },
+ cooldown: {
+ type: "INTEGER",
+ desc: "Set the cooldown between messages that add XP.",
+ val: 2
+ },
+ difficulty: {
+ type: "INTEGER",
+ desc: "Set the difficulty (ex: 2 will make it 2x harder to level up).",
+ val: 1
+ }
+ }
+ },
+ moderation: {
+ description: "Change where Sokora sends moderation logs.",
+ settings: {
+ channel: {
+ type: "TEXT",
+ desc: "ID of the log channel for moderation-related stuff (i.e a message being edited)."
+ },
+ log_messages: {
+ type: "BOOL",
+ desc: "Whether or not edited/deleted messages should be logged.",
+ val: true
+ }
+ }
+ },
+ news: {
+ description: "Configure news for your server.",
+ settings: {
+ channel_id: {
+ type: "TEXT",
+ desc: "ID of the channel where news messages are sent."
+ },
+ role_id: {
+ type: "TEXT",
+ desc: "ID of the roles that should be pinged when a news message is sent."
+ },
+ edit_original_message: {
+ type: "BOOL",
+ desc: "Whether or not the original message should be edited when a news message is updated.",
+ val: true
+ }
+ }
+ },
+ serverboard: {
+ description: "Configure your server's appearance on the serverboard.",
+ settings: {
+ shown: {
+ type: "BOOL",
+ desc: "Whether or not the server should be shown on the serverboard.",
+ val: false
+ }
+ }
+ },
+ welcome: {
+ description: "Change how Sokora welcomes your new users.",
+ settings: {
+ join_text: {
+ type: "TEXT",
+ desc: "Text sent when a user joins. (name) - username, (count) - member count, (servername) - server name.",
+ val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!"
+ },
+ leave_text: {
+ type: "TEXT",
+ desc: "Text sent when a user leaves. (name) - username, (count) - member count, (servername) - server name.",
+ val: "(name) has left the server! 😥"
+ },
+ channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." },
+ join_dm: {
+ type: "BOOL",
+ desc: "Whether or not the bot should send a custom DM message to the user upon joining.",
+ val: false
+ },
+ dm_text: {
+ type: "TEXT",
+ desc: "Text sent in the user's DM when they join the server. Same syntax as join_text.",
+ val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!"
+ }
+ }
+ },
+ easter: {
+ description: "Enable/disable easter eggs.",
+ settings: {
+ enabled: {
+ type: "BOOL",
+ desc: "Whether or not the bot should reply to certain messages with 'easter egg' messages.",
+ val: false
+ }
+ }
+ }
+};
+
+export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[];
+const database = getDatabase(tableDefinition);
+const getQuery = database.query("SELECT * FROM settings WHERE guildID = $1 AND key = $2;");
+const listPublicQuery = database.query(
+ "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = '1';"
+);
+const deleteQuery = database.query("DELETE FROM settings WHERE guildID = $1 AND key = $2;");
+const insertQuery = database.query(
+ "INSERT INTO settings (guildID, key, value) VALUES (?1, ?2, ?3);"
+);
+
+export function getSetting<
+ K extends keyof typeof settingsDefinition,
+ S extends keyof (typeof settingsDefinition)[K]["settings"]
+>(
+ guildID: string,
+ key: K,
+ setting: S
+): SqlType<(typeof settingsDefinition)[K]["settings"][S]["type"]> | null {
+ let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition<
+ typeof tableDefinition
+ >[];
+ const set = settingsDefinition[key].settings[setting];
+
+ if (!res.length) {
+ if (set.type == "LIST") return null;
+ return set.val;
+ }
+
+ switch (set.type) {
+ case "TEXT":
+ return res[0].value as SqlType;
+ case "BOOL":
+ return (res[0].value === "1" ? true : false) as SqlType;
+ case "INTEGER":
+ return parseInt(res[0].value) as SqlType;
+ case "LIST":
+ return kominator(res[0].value) as SqlType;
+ default:
+ return "WIP"; // as TypeOfKey;
+ }
+}
+
+export function setSetting(
+ guildID: string,
+ key: K,
+ setting: string,
+ value: string // TypeOfKey
+) {
+ const doInsert = getSetting(guildID, key, setting) == null;
+ if (!doInsert) deleteQuery.all(JSON.stringify(guildID), key + "." + setting);
+ insertQuery.run(JSON.stringify(guildID), `${key}.${setting}`, value);
+}
+
+export function listPublicServers() {
+ return (listPublicQuery.all() as TypeOfDefinition[]).map(entry =>
+ JSON.parse(entry.guildID)
+ );
+}
+
+// Utility type
+type TypeOfKey = SqlType<
+ (typeof settingsDefinition)[K]["settings"][S]["type"]
+>;
diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts
new file mode 100644
index 0000000..fb02252
--- /dev/null
+++ b/src/utils/database/types.ts
@@ -0,0 +1,18 @@
+export type FieldData = "TEXT" | "INTEGER" | "BOOL" | "TIMESTAMP" | "LIST";
+
+export type TableDefinition = {
+ name: string;
+ definition: Record;
+};
+
+export type SqlType = {
+ BOOL: boolean;
+ INTEGER: number;
+ TEXT: string;
+ TIMESTAMP: Date;
+ LIST: any[];
+}[T];
+
+export type TypeOfDefinition = {
+ [K in keyof T["definition"]]: SqlType;
+};
diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts
index f52808d..ee3e2e6 100644
--- a/src/utils/embeds/errorEmbed.ts
+++ b/src/utils/embeds/errorEmbed.ts
@@ -1,15 +1,26 @@
+import { EmbedBuilder, type ChatInputCommandInteraction, type ButtonInteraction } from "discord.js";
+import { genColor } from "../colorGen";
+
/**
* Sends the embed containing an error.
- * @param description Description of the error.
+ * @param interaction The interaction (slash command).
+ * @param title The error.
+ * @param reason The reason of the error.
* @returns Embed with the error description.
*/
-import { EmbedBuilder } from "discord.js";
-import { genColor } from "../colorGen.js";
-
-export default function errorEmbed(description: string) {
- return new EmbedBuilder()
- .setTitle("❌ • Error!")
- .setDescription(description)
+export async function errorEmbed(
+ interaction: ChatInputCommandInteraction | ButtonInteraction,
+ title: string,
+ reason?: string
+) {
+ const content = [`**${title}**`];
+ if (reason != undefined) content.push(reason);
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: "Something went wrong!" })
+ .setDescription(content.join("\n"))
.setColor(genColor(0));
+
+ if (interaction.replied) return await interaction.followUp({ embeds: [embed], ephemeral: true });
+ return await interaction.reply({ embeds: [embed], ephemeral: true });
}
diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts
new file mode 100644
index 0000000..e7ada38
--- /dev/null
+++ b/src/utils/embeds/modEmbed.ts
@@ -0,0 +1,155 @@
+import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js";
+import ms from "ms";
+import { genColor } from "../colorGen";
+import { addModeration, type modType } from "../database/moderation";
+import { logChannel } from "../logChannel";
+import { errorEmbed } from "./errorEmbed";
+
+type Options = {
+ interaction: ChatInputCommandInteraction;
+ user: User;
+ action: string;
+ duration?: string | null;
+ dm?: boolean;
+ dbAction?: modType;
+ expiresAt?: number;
+};
+
+type ErrorOptions = {
+ allErrors: boolean;
+ botError: boolean;
+ ownerError?: boolean;
+ outsideError?: boolean;
+};
+
+export async function errorCheck(
+ permission: bigint,
+ options: Options,
+ errorOptions: ErrorOptions,
+ permissionAction: string
+) {
+ const { interaction, user, action } = options;
+ const { allErrors, botError, ownerError, outsideError } = errorOptions;
+ const guild = interaction.guild!;
+ const members = guild.members.cache!;
+ const member = members.get(interaction.user.id)!;
+ const client = members.get(interaction.client.user.id)!;
+ const target = members.get(user.id)!;
+ const name = user.displayName;
+
+ if (botError)
+ if (!client.permissions.has(permission))
+ return await errorEmbed(
+ interaction,
+ "The bot can't execute this command.",
+ `The bot is missing the **${permissionAction}** permission.`
+ );
+
+ if (!member.permissions.has(permission))
+ return await errorEmbed(
+ interaction,
+ "You can't execute this command.",
+ `You're missing the **${permissionAction} Members** permission.`
+ );
+
+ if (!allErrors) return;
+ if (!target) return;
+ if (target == member)
+ return await errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`);
+
+ if (target.id == interaction.client.user.id)
+ return await errorEmbed(interaction, `You can't ${action.toLowerCase()} Sokora.`);
+
+ if (!target.manageable)
+ return await errorEmbed(
+ interaction,
+ `You can't ${action.toLowerCase()} ${name}.`,
+ "The member has a higher role position than Sokora."
+ );
+
+ if (member.roles.highest.position < target.roles.highest.position)
+ return await errorEmbed(
+ interaction,
+ `You can't ${action.toLowerCase()} ${name}.`,
+ "The member has a higher role position than you."
+ );
+
+ if (ownerError) {
+ if (target.id == guild.ownerId)
+ return await errorEmbed(
+ interaction,
+ `You can't ${action.toLowerCase()} ${name}.`,
+ "The member owns the server."
+ );
+ }
+
+ if (outsideError) {
+ if (
+ !(await guild.members
+ .fetch(user.id)
+ .then(() => true)
+ .catch(() => false))
+ )
+ return await errorEmbed(
+ interaction,
+ `You can't ${action.toLowerCase()} ${name}.`,
+ "This user isn't in this server."
+ );
+ }
+}
+
+export async function modEmbed(
+ options: Options,
+ reason?: string | null,
+ showModerator: boolean = false
+) {
+ const { interaction, user, action, duration, dm, dbAction, expiresAt } = options;
+ const guild = interaction.guild!;
+ const name = user.displayName;
+ const generalValues = [`**Moderator**: ${interaction.user.displayName}`];
+ let author = `• ${action} ${name}`;
+ reason ? generalValues.push(`**Reason**: ${reason}`) : generalValues.push("*No reason provided*");
+ if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`);
+ if (dbAction) {
+ try {
+ const id = addModeration(
+ guild.id,
+ user.id,
+ dbAction,
+ guild.members.cache.get(interaction.user.id)?.id!,
+ reason ?? undefined,
+ expiresAt ?? undefined
+ );
+ author = author.concat(` • #${id}`);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: author, iconURL: user.displayAvatarURL() })
+ .setDescription(generalValues.join("\n"))
+ .setFooter({ text: `User ID: ${user.id}` })
+ .setColor(genColor(100));
+
+ await logChannel(guild, embed);
+ if (interaction.replied) await interaction.followUp({ embeds: [embed] });
+ else await interaction.reply({ embeds: [embed] });
+
+ if (!dm) return;
+ const dmChannel = await user.createDM().catch(() => null);
+ if (!dmChannel || !guild.members.cache.get(user.id) || user.bot) return;
+ await dmChannel
+ .send({
+ embeds: [
+ embed
+ .setAuthor({
+ name: `• You got ${action.toLowerCase()}.`,
+ iconURL: user.displayAvatarURL()
+ })
+ .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n"))
+ .setColor(genColor(0))
+ ]
+ })
+ .catch(() => null);
+}
diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts
index 43fefed..fd0b716 100644
--- a/src/utils/embeds/serverEmbed.ts
+++ b/src/utils/embeds/serverEmbed.ts
@@ -4,130 +4,98 @@
* @returns Embed that contains the guild info.
*/
-import { ColorResolvable, EmbedBuilder, type Guild } from "discord.js";
-import { genColor, genRGBColor } from "../colorGen.js";
-import Vibrant from "node-vibrant";
-import sharp from "sharp";
-import database, { getServerboardTable } from "../database.js";
+import { EmbedBuilder, type Guild } from "discord.js";
+import { genColor } from "../colorGen";
+import { imageColor } from "../imageColor";
type Options = {
- guild: Guild
- roles?: boolean
- showInvite?: boolean
- page?: number
- pages?: number
- showSubs?: boolean
- subs?: number
+ guild: Guild;
+ roles?: boolean;
+ page?: number;
+ pages?: number;
};
-export default async function serverEmbed(options: Options) {
- const db = await database();
-
- const page = options.page;
- const pages = options.pages;
-
- // Retrieve guild information
- const guild = options.guild;
+export async function serverEmbed(options: Options) {
+ const { page, pages, guild } = options;
const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild;
- const iconURL = guild.iconURL();
-
- // Getting the invite table
- const showInvite = options.showInvite;
- const serverbTable = await getServerboardTable(db);
- const invite = await serverbTable?.get(`${guild.id}.invite`).then(
- async (invite) => invite ? String(invite) : null
- ).catch(() => null);
-
- // Getting different types of members
const members = guild.members.cache;
const boosters = members.filter(member => member.premiumSince);
- const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status)).size;
+ const onlineMembers = members.filter(member =>
+ ["online", "dnd", "idle"].includes(member.presence?.status!)
+ ).size;
const bots = members.filter(member => member.user.bot);
-
- // Formatting numbers to the American comma format
- const formattedMemberCount = guild.memberCount?.toLocaleString("en-US");
- const formattedOnlineMembers = onlineMembers?.toLocaleString("en-US");
const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US");
- const formattedBotCount = bots.size?.toLocaleString("en-US");
+ const icon = guild.iconURL()!;
- // Sorting the roles
const roles = guild.roles.cache;
- const rolesSorted = [...roles].sort((role1, role2) => role2[1].position - role1[1].position);
- rolesSorted.pop();
+ const sortedRoles = [...roles].sort((role1, role2) => role2[1].position - role1[1].position);
+ sortedRoles.pop();
+ const rolesLength = sortedRoles.length;
- // Organising the channel sizes
const channels = guild.channels.cache;
const channelSizes = {
- text: channels.filter(channel => channel.type === 0 || channel.type === 15 || channel.type === 5).size,
- voice: channels.filter(channel => channel.type === 2 || channel.type === 13).size,
- categories: channels.filter(channel => channel.type === 4).size
- }
+ text: channels.filter(channel => channel.type == 0 || channel.type == 15 || channel.type == 5)
+ .size,
+ voice: channels.filter(channel => channel.type == 2 || channel.type == 13).size,
+ categories: channels.filter(channel => channel.type == 4).size
+ };
- // Create the embed
const generalValues = [
- `**Owner**: <@${guild.ownerId}>`,
- `**Created on** `,
- ]
- if (options.showSubs) generalValues.push(`**Subscribers**: ${options.subs}`);
- if (showInvite && invite) generalValues.push(`**Invite link**: ${invite}`);
+ `Owned by **${(await guild.fetchOwner()).user.displayName}**`,
+ `Created on ****`
+ ];
const embed = new EmbedBuilder()
.setAuthor({
- name: `${pages ? `#${page} • ` : ""}${guild.name}`,
- url: showInvite && invite ? invite : null,
- iconURL: iconURL
+ name: `${pages ? `#${page} • ` : icon ? "• " : ""}${guild.name}`,
+ iconURL: icon
})
.setDescription(guild.description ? guild.description : null)
- .setFields({
- name: "📃 • General",
- value: generalValues.join("\n")
- })
- .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` })
- .setThumbnail(iconURL)
- .setColor(genColor(200));
-
- // Set the embed color
- try {
- const imageBuffer = await (await fetch(iconURL)).arrayBuffer(); // Stream the buffer
- const image = sharp(imageBuffer).toFormat("jpg");
- const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; // Get the most vibrant color
- embed.setColor(genRGBColor(r, g, b) as ColorResolvable);
- } catch { }
-
- // Adding the fields
- if (options.roles) embed.addFields({
- name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`,
- value: roles.size === 1
- ? "*None*"
- : `${rolesSorted.slice(0, 5).map(role => `<@&${role[0]}>`).join(", ")}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}`
- })
+ .setFields({ name: "📃 • General", value: generalValues.join("\n") })
+ .setFooter({ text: `${pages ? `Page ${page}/${pages}\n` : ""}Server ID: ${guild.id}` })
+ .setThumbnail(icon)
+ .setColor((await imageColor(icon)) ?? genColor(200));
+
+ if (options.roles)
+ embed.addFields({
+ name: `🎭 • ${roles.size - 1} ${roles.size == 1 ? "role" : "roles"}`,
+ value:
+ roles.size == 1
+ ? "*None*"
+ : `${sortedRoles
+ .slice(0, 5)
+ .map(role => `<@&${role[0]}>`)
+ .join(", ")}${rolesLength > 5 ? ` and **${rolesLength - 5}** more` : ""}`
+ });
embed.addFields(
{
- name: `👥 • ${formattedMemberCount} members`,
+ name: `👥 • ${guild.memberCount?.toLocaleString("en-US")} members`,
value: [
- `${formattedUserCount} users • ${formattedBotCount} bots`,
- `${formattedOnlineMembers} online`
+ `**${formattedUserCount}** users • **${bots.size?.toLocaleString("en-US")}** bots`,
+ `**${onlineMembers?.toLocaleString("en-US")}** online`
].join("\n"),
inline: true
},
{
name: `🗨️ • ${channelSizes.text + channelSizes.voice} channels`,
value: [
- `${channelSizes.text} text • ${channelSizes.voice} voice`,
- `${channelSizes.categories} categories`
+ `**${channelSizes.text}** text • **${channelSizes.voice}** voice`,
+ `**${channelSizes.categories}** categories`
].join("\n"),
inline: true
},
{
- name: `🌟 • ${boostCount}${boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : ""} boosts`,
+ name: `🌟 • ${!boostTier ? "No level" : `Level ${boostTier}`}`,
value: [
- boostTier == 0 ? "No boosts" : `Level ${boostTier}`,
- `${boosters.size} ${boosters.size === 1 ? "booster" : "boosters"}`
+ `**${boostCount}**${
+ !boostTier ? "/2" : boostTier == 1 ? "/7" : boostTier == 2 ? "/14" : ""
+ } boosts`,
+ `**${boosters.size}** ${boosters.size == 1 ? "booster" : "boosters"}`
].join("\n"),
inline: true
}
- )
+ );
return embed;
}
diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts
new file mode 100644
index 0000000..22a162c
--- /dev/null
+++ b/src/utils/imageColor.ts
@@ -0,0 +1,22 @@
+/**
+ * Outputs the most vibrant color from the image.
+ * @param guild Guild image.
+ * @param member Member image.
+ * @returns The color in HEX.
+ */
+
+import type { ColorResolvable } from "discord.js";
+import Vibrant from "node-vibrant";
+import sharp from "sharp";
+import { genRGBColor } from "./colorGen";
+
+export async function imageColor(guildURL?: string, memberURL?: string) {
+ if (!guildURL || !memberURL) return;
+
+ const imageBuffer = await (await fetch(guildURL ?? memberURL)).arrayBuffer();
+ const { r, g, b } = (
+ await new Vibrant(await sharp(imageBuffer).toFormat("jpg").toBuffer()).getPalette()
+ ).Vibrant!;
+
+ return genRGBColor(Math.round(r), Math.round(g), Math.round(b)) as ColorResolvable;
+}
diff --git a/src/utils/kominator.ts b/src/utils/kominator.ts
new file mode 100644
index 0000000..b1c8298
--- /dev/null
+++ b/src/utils/kominator.ts
@@ -0,0 +1,9 @@
+/**
+ * Splits a string using commas.
+ * @param string String to split.
+ * @returns An array of strings from the original string.
+ */
+
+export function kominator(string: string): string[] {
+ return string.split(",").map(str => str.replace('"', "").trim());
+}
diff --git a/src/utils/logChannel.ts b/src/utils/logChannel.ts
new file mode 100644
index 0000000..a733617
--- /dev/null
+++ b/src/utils/logChannel.ts
@@ -0,0 +1,38 @@
+/**
+ * Sends a message in the log channel. (if there is one set)
+ * @param guild The guild where the log channel is located.
+ * @param embed Embed of the log.
+ * @returns Log message.
+ */
+
+import {
+ ChannelType,
+ type Channel,
+ type EmbedBuilder,
+ type Guild,
+ type TextChannel
+} from "discord.js";
+import { getSetting } from "./database/settings";
+
+export async function logChannel(guild: Guild, embed: EmbedBuilder) {
+ const logChannel = getSetting(guild.id, "moderation", "channel");
+ if (!logChannel) return;
+
+ const channel = await guild.channels.cache
+ .get(`${logChannel}`)
+ ?.fetch()
+ .then((channel: Channel) => {
+ if (
+ channel.type != ChannelType.GuildText &&
+ ChannelType.PublicThread &&
+ ChannelType.PrivateThread &&
+ ChannelType.GuildVoice
+ )
+ return null;
+
+ return channel as TextChannel;
+ })
+ .catch(() => null);
+
+ if (channel) return await channel.send({ embeds: [embed] });
+}
diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts
index 03d5be5..dfbf46e 100644
--- a/src/utils/multiReact.ts
+++ b/src/utils/multiReact.ts
@@ -1,12 +1,17 @@
+/**
+ * Reacts to a message with multiple emojis.
+ * @param message Message to react to.
+ * @param emojis Emojis that will be used to react.
+ */
+
import type { Message } from "discord.js";
-export default async function multiReact(message: Message, ...reactions) {
- for (const i of reactions) {
- if (typeof i === "object") {
+export async function multiReact(message: Message, ...emojis: string[]) {
+ for (const i of emojis) {
+ if (typeof i == "object") {
await message.react(i);
continue;
}
-
- for (const reaction of i) if (reaction !== " ") await message.react(reaction);
+ for (const reaction of i) if (reaction != " ") await message.react(reaction);
}
}
diff --git a/src/utils/quickSort.ts b/src/utils/quickSort.ts
index 9aa40b2..38beb94 100644
--- a/src/utils/quickSort.ts
+++ b/src/utils/quickSort.ts
@@ -1,18 +1,20 @@
type Corresponding = [...any[]] | null;
-// Swap the values
-function swap(sortItems: number[], corresponding: Corresponding, leftIndex: number, rightIndex: number) {
+function swap(
+ sortItems: number[],
+ corresponding: Corresponding,
+ leftIndex: number,
+ rightIndex: number
+) {
let temp = sortItems[leftIndex];
sortItems[leftIndex] = sortItems[rightIndex];
sortItems[rightIndex] = temp;
- // Swap the corresponding values
if (!corresponding) return;
for (let i = 0; i < corresponding.length; i++) {
let subArray = corresponding[i];
if (Array.isArray(subArray)) {
- // Swap the corresponding sub-array values
let tempValue = subArray[leftIndex];
subArray[leftIndex] = subArray[rightIndex];
subArray[rightIndex] = tempValue;
@@ -20,56 +22,40 @@ function swap(sortItems: number[], corresponding: Corresponding, leftIndex: numb
}
}
-// Partition the array
-function partition(sortItems: number[], corresponding: Corresponding, leftIndex: number, rightIndex: number): number {
- let pivot: number = sortItems[Math.floor((rightIndex + leftIndex) / 2)], // middle element
- leftPointer: number = leftIndex, // left pointer
- rightPointer: number = rightIndex; // right pointer
+/**
+ * Sorts an array of items and returns the sorted items and corresponding items.
+ * @param sortItems The items to sort.
+ * @param corresponding The corresponding items to sort.
+ * @param leftIndex The left index of the sortItems array.
+ * @param rightIndex The right index of the sortItems array.
+ * @returns The sorted items and corresponding items.
+ */
+export function quickSort(
+ sortItems: number[],
+ corresponding: Corresponding,
+ leftIndex: number,
+ rightIndex: number
+): [number[], Corresponding] {
+ let pivot = sortItems[Math.floor((rightIndex + leftIndex) / 2)];
+ let leftPointer = leftIndex;
+ let rightPointer = rightIndex;
while (leftPointer <= rightPointer) {
while (sortItems[leftPointer] < pivot) leftPointer++;
while (sortItems[rightPointer] > pivot) rightPointer--;
if (leftPointer <= rightPointer) {
- swap(sortItems, corresponding, leftPointer, rightPointer); // swapping two elements
+ swap(sortItems, corresponding, leftPointer, rightPointer);
leftPointer++;
rightPointer--;
}
}
- return leftPointer;
-}
-
-/**
- * Sorts an array of items and returns the sorted items and corresponding items
- * @param sortItems The items to sort
- * @param corresponding The corresponding items to sort, but linked to the exact index of the sortItems array (input [[...], [...], ...]) (Optional)
- * @param leftIndex The left index of the sortItems array
- * @param rightIndex The right index of the sortItems array
- * @returns The sorted items and corresponding items
- */
-// Initialize the quicksort function
-export default function quickSort(
- sortItems: number[],
- corresponding: Corresponding,
- leftIndex: number,
- rightIndex: number
-): [number[], Corresponding] {
- let index: number;
-
if (sortItems.length > 1) {
- index = partition(sortItems, corresponding, leftIndex, rightIndex); // index returned from partition
-
- if (leftIndex < index - 1) {
- // more elements on the left side of the pivot
- quickSort(sortItems, corresponding, leftIndex, index - 1);
- }
-
- if (index < rightIndex) {
- // more elements on the right side of the pivot
- quickSort(sortItems, corresponding, index, rightIndex);
- }
+ if (leftIndex < leftPointer - 1)
+ quickSort(sortItems, corresponding, leftIndex, leftPointer - 1);
+ if (leftPointer < rightIndex) quickSort(sortItems, corresponding, leftPointer, rightIndex);
}
- return [sortItems, corresponding]; // Return both sorted items and corresponding array
+ return [sortItems, corresponding];
}
diff --git a/src/utils/randomise.ts b/src/utils/randomise.ts
index a1c99ad..b526712 100644
--- a/src/utils/randomise.ts
+++ b/src/utils/randomise.ts
@@ -4,6 +4,6 @@
* @returns Randomised value from within the array.
*/
-export default function randomise(array: any[]) {
+export function randomise(array: any[]) {
return array[Math.floor(Math.random() * array.length)];
}
diff --git a/src/utils/replace.ts b/src/utils/replace.ts
new file mode 100644
index 0000000..2d0f6a6
--- /dev/null
+++ b/src/utils/replace.ts
@@ -0,0 +1,10 @@
+import type { GuildMember, EmbedBuilder } from "discord.js";
+
+export function replace(member: GuildMember, text: string, embed: EmbedBuilder) {
+ const user = member.user;
+ const guild = member.guild;
+ if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName);
+ if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`);
+ if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`);
+ embed.setDescription(text);
+}
diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts
index 9bc231e..b45eb2c 100644
--- a/src/utils/sendChannelNews.ts
+++ b/src/utils/sendChannelNews.ts
@@ -1,53 +1,52 @@
+/**
+ * Sends news to a channel.
+ * @param guild Guild where the channel is in.
+ * @param id ID of the news.
+ * @param interaction Command nteraction.
+ * @param title Title of the news.
+ * @param body Content of the news.
+ * @returns News message in a channel.
+ */
+
import {
- EmbedBuilder, Guild, Role,
- TextChannel
+ EmbedBuilder,
+ type ChatInputCommandInteraction,
+ type Guild,
+ type Role,
+ type TextChannel
} from "discord.js";
-import database, { getNewsTable } from "./database.js";
-import { genColor } from "./colorGen.js";
-
-export type News = {
- title: string
- body: string
- imageURL: string
- author: string
- authorPfp: string
- createdAt: string
- updatedAt: string
- messageId?: string
-}
-
-export default async function sendChannelNews(guild: Guild, news: News, id: string) {
- const db = await database();
- const newsTable = await getNewsTable(db);
+import { genColor } from "./colorGen";
+import { get, updateNews } from "./database/news";
+import { getSetting } from "./database/settings";
- const subscribedChannel = await newsTable.get(`${guild.id}.channel`).then(
- (channel) => channel as { channelId: string | null, roleId: string | null }
- ).catch(() => {
- return {
- channelId: null as string | null,
- roleId: null as string | null
- };
- });
-
- if (!subscribedChannel) return;
- const channel = subscribedChannel.channelId;
- const channelToSend = guild.channels.cache.get(channel) as TextChannel;
- if (!channelToSend) return;
-
- const role = subscribedChannel.roleId;
- let roleToSend: Role | null;
+export async function sendChannelNews(
+ guild: Guild,
+ id: string,
+ interaction: ChatInputCommandInteraction,
+ title?: string,
+ body?: string
+) {
+ const news = get(id)!;
+ const role = getSetting(guild.id, "news", "role_id") as string;
+ let roleToSend: Role | undefined;
if (role) roleToSend = guild.roles.cache.get(role);
- const newsEmbed = new EmbedBuilder()
- .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null })
- .setTitle(news.title)
- .setDescription(news.body)
- .setImage(news.imageURL || null)
- .setTimestamp(parseInt(news.updatedAt))
- .setFooter({ text: `Latest news from ${guild.name}` })
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP })
+ .setTitle(title ?? news.title)
+ .setDescription(body ?? news.body)
+ .setTimestamp(parseInt(news.updatedAt.toString()) ?? null)
+ .setFooter({ text: `Latest news from ${guild.name}\nID: ${news.id}` })
.setColor(genColor(200));
- const message = await channelToSend.send({ embeds: [newsEmbed], content: roleToSend ? `<@&${roleToSend.id}>` : null });
- await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id);
- return;
-}
\ No newline at end of file
+ return (
+ guild.channels.cache.get(
+ (getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id
+ ) as TextChannel
+ )
+ .send({
+ embeds: [embed],
+ content: roleToSend ? `<@&${roleToSend.id}>` : undefined
+ })
+ .then(message => updateNews(id, undefined, undefined, message.id));
+}
diff --git a/src/utils/sendSubscribedNews.ts b/src/utils/sendSubscribedNews.ts
deleted file mode 100644
index 1ce906f..0000000
--- a/src/utils/sendSubscribedNews.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { DMChannel, EmbedBuilder, Guild } from "discord.js";
-import database, { getNewsTable } from "./database.js";
-import { genColor } from "./colorGen.js";
-
-export type News = {
- title: string
- body: string
- imageURL: string
- author: string
- authorPfp: string
- createdAt: string
- updatedAt: string
- messageId?: string
-}
-
-export default async function sendSubscribedNews(guild: Guild, news: News) {
- const db = await database();
- const newsTable = await getNewsTable(db);
-
- const subscriptions = await newsTable.get(`${guild.id}.subscriptions`).then(
- (subscriptions) => subscriptions as string[] ?? [] as string[]
- ).catch(() => [] as string[]);
- const members = await guild.members.fetch();
- const subscribed = members.filter((member) => subscriptions.includes(member.id));
- const memberDMs = (await Promise.all(subscribed.map((member) => member.createDM().catch(() => null)))) as DMChannel[] | null;;
- const memberDMsOpen = memberDMs.filter((dm) => dm !== null);
-
- const newsEmbed = new EmbedBuilder()
- .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null })
- .setTitle(news.title)
- .setDescription(news.body)
- .setImage(news.imageURL || null)
- .setTimestamp(parseInt(news.updatedAt))
- .setFooter({ text: `Latest news from ${guild.name}` })
- .setColor(genColor(200));
-
- await Promise.all(memberDMsOpen.map((dm) => dm.send({ embeds: [newsEmbed] }).catch(() => null)));
- return;
-}
diff --git a/src/utils/types.ts b/src/utils/types.ts
new file mode 100644
index 0000000..9e8daf0
--- /dev/null
+++ b/src/utils/types.ts
@@ -0,0 +1,3 @@
+import { ClientEvents } from "discord.js";
+
+export type Event = (...args: ClientEvents[T][0][]) => any;
diff --git a/src/utils/unbanScheduler.ts b/src/utils/unbanScheduler.ts
new file mode 100644
index 0000000..599fccf
--- /dev/null
+++ b/src/utils/unbanScheduler.ts
@@ -0,0 +1,57 @@
+import { Client, EmbedBuilder } from "discord.js";
+import { genColor } from "./colorGen";
+import { getPendingBans, removeModeration } from "./database/moderation";
+import { logChannel } from "./logChannel";
+
+export function scheduleUnban(
+ client: Client,
+ guildID: string,
+ userID: string,
+ modID: string,
+ delay: number
+) {
+ const scheduledUnbans = new Map();
+ const key = `${guildID}-${userID}`;
+ if (scheduledUnbans.has(key)) clearTimeout(scheduledUnbans.get(key)!);
+
+ const timeout = setTimeout(async () => {
+ try {
+ const guild = await client.guilds.fetch(guildID);
+ const user = guild.bans.cache.get(userID)?.user!;
+ const moderator = guild.members.cache.get(modID)!;
+ const embed = new EmbedBuilder()
+ .setAuthor({ name: `• Unbanned ${user.displayName}`, iconURL: user.displayAvatarURL() })
+ .setDescription(
+ [`**Moderator**: ${moderator.displayName}`, "*Temporary ban has expired*"].join("\n")
+ )
+ .setFooter({ text: `User ID: ${user.id}` })
+ .setColor(genColor(100));
+
+ await logChannel(guild, embed);
+ await guild.members.unban(userID, "Temporary ban has expired");
+ removeModeration(guildID, userID);
+ scheduledUnbans.delete(key);
+ } catch (error) {
+ console.error(`Failed to unban user ${userID} in guild ${guildID}:`, error);
+ }
+ }, delay);
+
+ return scheduledUnbans.set(key, timeout);
+}
+
+export function rescheduleUnbans(client: Client) {
+ const now = Date.now();
+ const pendingBans = getPendingBans(now);
+
+ for (const ban of pendingBans) {
+ if (!ban.expiresAt) continue;
+ if (typeof ban.expiresAt !== "number" || isNaN(ban.expiresAt)) {
+ console.error(`Invalid expiresAt value for ban: ${ban.expiresAt}`);
+ continue;
+ }
+
+ const delay = ban.expiresAt - now;
+ if (delay > 0) scheduleUnban(client, ban.guild, ban.user, ban.moderator, delay);
+ else removeModeration(ban.guild, ban.id);
+ }
+}
diff --git a/src/utils/validateURL.ts b/src/utils/validateURL.ts
deleted file mode 100644
index 7b6857e..0000000
--- a/src/utils/validateURL.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export default function validateURL(url) {
- if (/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/g.test(url)) return true;
- return false;
-}
diff --git a/static/banner.png b/static/banner.png
new file mode 100644
index 0000000..a4dfb93
Binary files /dev/null and b/static/banner.png differ
diff --git a/static/banner.svg b/static/banner.svg
deleted file mode 100644
index faa565e..0000000
--- a/static/banner.svg
+++ /dev/null
@@ -1,60 +0,0 @@
-
diff --git a/tsconfig.json b/tsconfig.json
index e4ce5f9..1a7a764 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,20 +1,20 @@
{
"compilerOptions": {
- "target": "ESNext",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
- "noImplicitAny": false,
- "removeComments": true,
- "preserveConstEnums": true,
- "forceConsistentCasingInFileNames": true,
- "esModuleInterop": true,
+ "lib": ["ESNext"],
+ "module": "esnext",
+ "target": "esnext",
+ "moduleResolution": "Node",
+ "moduleDetection": "force",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "noEmit": true,
+ "composite": true,
+ "strict": true,
+ "downlevelIteration": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
- "noFallthroughCasesInSwitch": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "strict": false,
+ "forceConsistentCasingInFileNames": true,
+ "allowJs": true,
"types": ["bun-types"]
- },
- "exclude": ["node_modules"],
-}
+ }
+}
\ No newline at end of file