diff --git a/.cspell/custom-dictionary-workspace.txt b/.cspell/custom-dictionary-workspace.txt index d754312..f5e0cc5 100644 --- a/.cspell/custom-dictionary-workspace.txt +++ b/.cspell/custom-dictionary-workspace.txt @@ -23,6 +23,7 @@ pino Poäng Profil rando +Repliable satta senaste Sifell diff --git a/README.md b/README.md index c6ca509..c73ca8a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
-

An multi-purpose bot built with discord.js

+

A privacy-focused bot built with discord.js

@@ -15,61 +15,7 @@

- About • - Features + Documentation • - Installation - • - License - • - Credits

- -## ❓ About - -Xyter is an open source, multi-purpose Discord bot that is develoepd by students. You can invite it to your Discord server using [this](https://bot.zyner.org) link! It comes packaged with a variety of commands and a multitude of settings that can be tailored to your server's specific needs. - -**To run this on Pterodactyl** please set startup command to `./node_modules/.bin/ts-node src/index.ts`! - -If you liked this repository, feel free to leave a star ⭐ to help promote Xyter! - -**For a more updated documentation visit [this site](https://xyter.zyner.org/)!** - -## ❗ Features - -**10+** commands and counting across **13** different categories! - -- 💰 **Credits**: `balance`, `gift`, `top`, `work`, `give`, `take`, `set` and `transfer`! -- 💬 **Counters**: `view`, `add`, `remove`! -- 🔨 **Settings**: `guild credits`, `guild pterodactyl`, `guild points` and `user appearence`! -- 👑 **Profile**: `view`! -- 🖼 **Reputation**: `give`! -- 💰 **Shop**: `roles buy`, `roles cancel` and `pterodactyl`! -- ❔ **Utilities**: `lookup`, `about` and `stats`! -- **Full list** of commands: [here](https://github.com/ZynerOrg/xyter/blob/master/docs/COMMANDS.md). - -Xyter also comes packed with a variety of features, such as: - -- **Slash Commands** -- **Multi-language support**. -- And much more! There are over **5+** settings to tweak! - -## 📝 To-Do - -- Bug fixes -- Code optimisation -- New discord features -- Suggestions we deem very good. - -Some more is available in issues - -## 📖 License - -Released under the [GPL-3.0 License](https://github.com/ZynerOrg/xyter/blob/master/LICENSE) license. - -## 📜 Credits - -- **[Vermium#9649](https://github.com/VermiumSifell)** - Founder, creator, hoster. -- **[Mastergamer433#5762](https://github.com/Mastergamer433)** - Work command for credits. -- Want to be on this list, aswell? - Check out the [Contributing page](https://github.com/ZynerOrg/xyter/blob/master/docs/CONTRIBUTING.md). diff --git a/src/handlers/deployCommands.ts b/src/handlers/deployCommands/index.ts similarity index 88% rename from src/handlers/deployCommands.ts rename to src/handlers/deployCommands/index.ts index 4542377..492e1ab 100644 --- a/src/handlers/deployCommands.ts +++ b/src/handlers/deployCommands/index.ts @@ -1,13 +1,13 @@ -import { token, clientId } from "../config/discord"; -import { devMode, guildId } from "../config/other"; +import { token, clientId } from "../../config/discord"; +import { devMode, guildId } from "../../config/other"; -import logger from "../logger"; +import logger from "../../logger"; import { Client } from "discord.js"; import { REST } from "@discordjs/rest"; import { Routes } from "discord-api-types/v9"; import { RESTPostAPIApplicationCommandsJSONBody } from "discord-api-types/v10"; -import { ICommand } from "../interfaces/Command"; +import { ICommand } from "../../interfaces/Command"; export default async (client: Client) => { const commandList: Array = []; diff --git a/src/handlers/devMode.ts b/src/handlers/devMode/index.ts similarity index 79% rename from src/handlers/devMode.ts rename to src/handlers/devMode/index.ts index 764fbcf..c7900a0 100644 --- a/src/handlers/devMode.ts +++ b/src/handlers/devMode/index.ts @@ -1,10 +1,10 @@ // Dependencies import { Client } from "discord.js"; -import logger from "../logger"; +import logger from "../../logger"; // Configuration -import { devMode, guildId } from "../config/other"; +import { devMode, guildId } from "../../config/other"; export default async (client: Client) => { if (!devMode) { diff --git a/src/handlers/encryption.ts b/src/handlers/encryption/index.ts similarity index 84% rename from src/handlers/encryption.ts rename to src/handlers/encryption/index.ts index de5a7b9..8b6512c 100644 --- a/src/handlers/encryption.ts +++ b/src/handlers/encryption/index.ts @@ -1,8 +1,8 @@ import crypto from "crypto"; -import { secretKey, algorithm } from "../config/encryption"; +import { secretKey, algorithm } from "../../config/encryption"; -import { IEncryptionData } from "../interfaces/EncryptionData"; +import { IEncryptionData } from "../../interfaces/EncryptionData"; const iv = crypto.randomBytes(16); diff --git a/src/helpers/addSeconds/index.ts b/src/helpers/addSeconds/index.ts index 91bff4e..6c47b18 100644 --- a/src/helpers/addSeconds/index.ts +++ b/src/helpers/addSeconds/index.ts @@ -1,7 +1,4 @@ -export default async (numOfSeconds: number, date: Date) => { - if (!numOfSeconds) throw new Error("numOfSeconds is required"); - - date.setSeconds(date.getSeconds() + numOfSeconds); - +export default async (seconds: number, date: Date) => { + date.setSeconds(date.getSeconds() + seconds); return date; }; diff --git a/src/helpers/cooldown/index.ts b/src/helpers/cooldown/index.ts index 2417d81..45d2729 100644 --- a/src/helpers/cooldown/index.ts +++ b/src/helpers/cooldown/index.ts @@ -1,5 +1,5 @@ // Dependencies -import { CommandInteraction, Message } from "discord.js"; +import { CommandInteraction, ButtonInteraction, Message } from "discord.js"; import logger from "../../logger"; @@ -7,7 +7,7 @@ import getEmbedConfig from "../../helpers/getEmbedConfig"; import timeoutSchema from "../../models/timeout"; import addSeconds from "../../helpers/addSeconds"; -export const interaction = async (i: CommandInteraction, cooldown: number) => { +export const command = async (i: CommandInteraction, cooldown: number) => { const { guild, user, commandId } = i; // Check if user has a timeout @@ -56,12 +56,57 @@ export const interaction = async (i: CommandInteraction, cooldown: number) => { }); }; -export const message = async ( - message: Message, - cooldown: number, - id: string -) => { - const { guild, member } = message; +export const button = async (i: ButtonInteraction, cooldown: number) => { + const { guild, user, customId } = i; + + // Check if user has a timeout + const hasTimeout = await timeoutSchema.findOne({ + guildId: guild?.id || "0", + userId: user.id, + cooldown: cooldown, + timeoutId: customId, + }); + + // If user is not on timeout + if (hasTimeout) { + const { guildId, userId, timeoutId, createdAt } = hasTimeout; + const overDue = (await addSeconds(cooldown, createdAt)) < new Date(); + + if (!overDue) { + const diff = Math.round( + (new Date(hasTimeout.createdAt).getTime() - new Date().getTime()) / 1000 + ); + + throw new Error( + `You must wait ${diff} seconds before using this command.` + ); + } + + // Delete timeout + await timeoutSchema + .deleteOne({ + guildId, + userId, + timeoutId, + cooldown, + }) + .then(async () => { + logger.debug( + `Timeout document ${timeoutId} has been deleted from user ${userId}.` + ); + }); + } + // Create timeout + await timeoutSchema.create({ + guildId: guild?.id || "0", + userId: user.id, + cooldown: cooldown, + timeoutId: customId, + }); +}; + +export const message = async (msg: Message, cooldown: number, id: string) => { + const { guild, member } = msg; if (!guild) throw new Error("Guild is undefined"); if (!member) throw new Error("Member is undefined"); diff --git a/src/helpers/deferReply/index.ts b/src/helpers/deferReply/index.ts index b5cfdc3..02cc413 100644 --- a/src/helpers/deferReply/index.ts +++ b/src/helpers/deferReply/index.ts @@ -1,25 +1,26 @@ -import { CommandInteraction, MessageEmbed } from "discord.js"; +import { Interaction, MessageEmbed } from "discord.js"; import getEmbedConfig from "../../helpers/getEmbedConfig"; -export default async (interaction: CommandInteraction, ephemeral: boolean) => { +export default async (interaction: Interaction, ephemeral: boolean) => { + if (!interaction.isRepliable()) + throw new Error(`Cannot reply to an interaction that is not repliable`); + await interaction.deferReply({ ephemeral, }); - const { waitColor, footerText, footerIcon } = await getEmbedConfig( - interaction.guild - ); + const embedConfig = await getEmbedConfig(interaction.guild); await interaction.editReply({ embeds: [ new MessageEmbed() .setFooter({ - text: footerText, - iconURL: footerIcon, + text: embedConfig.footerText, + iconURL: embedConfig.footerIcon, }) .setTimestamp(new Date()) .setTitle("Processing your request") - .setColor(waitColor) + .setColor(embedConfig.waitColor) .setDescription("Please wait..."), ], }); diff --git a/src/helpers/getEmbedConfig/index.ts b/src/helpers/getEmbedConfig/index.ts index 22ddf29..ccd3fdb 100644 --- a/src/helpers/getEmbedConfig/index.ts +++ b/src/helpers/getEmbedConfig/index.ts @@ -3,18 +3,16 @@ import * as embedConfig from "../../config/embed"; import { Guild } from "discord.js"; -export default async (guild: Guild | null) => { - if (guild == null) - return { - ...embedConfig, - }; +export default async (guild?: Guild | null) => { + if (!guild) { + return { ...embedConfig }; + } const guildConfig = await guildSchema.findOne({ guildId: guild.id }); - - if (guildConfig == null) + if (!guildConfig) { return { ...embedConfig, }; - + } return guildConfig.embeds; }; diff --git a/src/helpers/listDir/index.ts b/src/helpers/listDir/index.ts index 95ed1da..382e41d 100644 --- a/src/helpers/listDir/index.ts +++ b/src/helpers/listDir/index.ts @@ -2,7 +2,7 @@ import fs from "fs"; const fsPromises = fs.promises; export default async (path: string) => { - return fsPromises.readdir(path).catch(async (e) => { - throw new Error(`Could not list directory: ${path}`, e); + return fsPromises.readdir(path).catch(async (err) => { + throw new Error(`Could not list directory: ${path}`, err); }); }; diff --git a/src/helpers/updatePresence/index.ts b/src/helpers/updatePresence/index.ts index a8c4a35..334a3e2 100644 --- a/src/helpers/updatePresence/index.ts +++ b/src/helpers/updatePresence/index.ts @@ -5,15 +5,12 @@ import logger from "../../logger"; // Function export default async (client: Client) => { if (!client?.user) throw new Error("Client's user is undefined."); - const { guilds } = client; const memberCount = guilds.cache.reduce((a, g) => a + g.memberCount, 0); - const guildCount = guilds.cache.size; const status = `${memberCount} users in ${guildCount} guilds.`; - client.user.setPresence({ activities: [{ type: "LISTENING", name: status }], status: "online", diff --git a/src/interfaces/ShopRole.ts b/src/interfaces/ShopRole.ts new file mode 100644 index 0000000..ec62cfd --- /dev/null +++ b/src/interfaces/ShopRole.ts @@ -0,0 +1,9 @@ +import { Snowflake } from "discord.js"; +import { Document } from "mongoose"; + +export interface IShopRole extends Document { + guildId: Snowflake; + userId: Snowflake; + roleId: Snowflake; + lastPayed: Date; +} diff --git a/src/jobs/shop/index.ts b/src/jobs/shop/index.ts new file mode 100644 index 0000000..b2ef366 --- /dev/null +++ b/src/jobs/shop/index.ts @@ -0,0 +1,12 @@ +// Dependencies +import { Client } from "discord.js"; + +import * as roles from "./modules/roles"; + +export const options = { + schedule: "*/5 * * * *", // https://crontab.guru/ +}; + +export const execute = async (client: Client) => { + await roles.execute(client); +}; diff --git a/src/jobs/shop/modules/roles/components/dueForPayment.ts b/src/jobs/shop/modules/roles/components/dueForPayment.ts new file mode 100644 index 0000000..ad8a59c --- /dev/null +++ b/src/jobs/shop/modules/roles/components/dueForPayment.ts @@ -0,0 +1,10 @@ +import { Client } from "discord.js"; +import logger from "../../../../../logger"; + +import { IShopRole } from "../../../../../interfaces/ShopRole"; + +export const execute = async (_client: Client, role: IShopRole) => { + const { roleId } = role; + + logger.silly(`Shop role ${roleId} is not due for payment.`); +}; diff --git a/src/jobs/shop/modules/roles/components/overDueForPayment.ts b/src/jobs/shop/modules/roles/components/overDueForPayment.ts new file mode 100644 index 0000000..77ab6e0 --- /dev/null +++ b/src/jobs/shop/modules/roles/components/overDueForPayment.ts @@ -0,0 +1,85 @@ +import { Client } from "discord.js"; +import logger from "../../../../../logger"; + +import { IShopRole } from "../../../../../interfaces/ShopRole"; +import guildSchema from "../../../../../models/guild"; +import userSchema from "../../../../../models/user"; +import shopRoleSchema from "../../../../../models/shopRole"; + +export const execute = async (client: Client, role: IShopRole) => { + const { guildId, userId, roleId } = role; + if (!userId) throw new Error("User ID not found for shop role."); + + const guildData = await guildSchema.findOne({ guildId }); + if (!guildData) throw new Error("Guild not found."); + + const userData = await userSchema.findOne({ guildId, userId }); + if (!userData) throw new Error("User not found."); + + const rGuild = client.guilds.cache.get(guildId); + if (!rGuild) throw new Error("Guild not found."); + + const rMember = await rGuild.members.fetch(userId); + if (!rMember) throw new Error("Member not found."); + + const rRole = rMember.roles.cache.get(roleId); + if (!rRole) throw new Error("Role not found."); + + logger.debug(`Shop role ${roleId} is due for payment.`); + + const { pricePerHour } = guildData.shop.roles; + + if (userData.credits < pricePerHour) { + await rMember.roles + .remove(roleId) + .then(async () => { + await shopRoleSchema + .deleteOne({ + userId, + roleId, + guildId, + }) + .then(async () => { + logger.silly( + `Shop role document ${roleId} has been deleted from user ${userId}.` + ); + }) + .catch(async (err) => { + throw new Error( + `Error deleting shop role document ${roleId} from user ${userId}.`, + err + ); + }); + }) + .catch(async (err) => { + throw new Error( + `Error removing role ${roleId} from user ${userId}.`, + err + ); + }); + + throw new Error("User does not have enough credits."); + } + + userData.credits -= pricePerHour; + await userData + .save() + .then(async () => { + logger.silly(`User ${userId} has been updated.`); + + role.lastPayed = new Date(); + await role + .save() + .then(async () => { + logger.silly(`Shop role ${roleId} has been updated.`); + }) + .catch(async (err) => { + throw new Error(`Error updating shop role ${roleId}.`, err); + }); + + logger.debug(`Shop role ${roleId} has been paid.`); + }) + .catch(async (err) => { + throw new Error(`Error updating user ${userId}.`, err); + }); +}; diff --git a/src/jobs/shop/modules/roles/index.ts b/src/jobs/shop/modules/roles/index.ts new file mode 100644 index 0000000..c8e393a --- /dev/null +++ b/src/jobs/shop/modules/roles/index.ts @@ -0,0 +1,32 @@ +import { Client } from "discord.js"; + +import { IShopRole } from "../../../../interfaces/ShopRole"; +import shopRoleSchema from "../../../../models/shopRole"; + +import * as overDueForPayment from "./components/overDueForPayment"; +import * as dueForPayment from "./components/dueForPayment"; + +export const execute = async (client: Client) => { + const roles = await shopRoleSchema.find(); + + await Promise.all( + roles.map(async (role: IShopRole) => { + const { lastPayed } = role; + const nextPayment = new Date( + lastPayed.setHours(lastPayed.getHours() + 1) + ); + + const now = new Date(); + + if (nextPayment > now) { + await dueForPayment.execute(client, role); + + return; + } + + if (nextPayment < now) { + await overDueForPayment.execute(client, role); + } + }) + ); +}; diff --git a/src/jobs/shopRoles.ts b/src/jobs/shopRoles.ts deleted file mode 100644 index e9ed502..0000000 --- a/src/jobs/shopRoles.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Dependencies -import { Client } from "discord.js"; - -import logger from "../logger"; - -// Schemas -import userSchema from "../models/user"; -import shopRoleSchema from "../models/shopRole"; -import guildSchema from "../models/guild"; - -export const options = { - schedule: "*/5 * * * *", // https://crontab.guru/ -}; - -export const execute = async (client: Client) => { - const roles = await shopRoleSchema.find(); - await Promise.all( - roles.map(async (role) => { - const { guildId, userId, roleId } = role; - const lastPayment = new Date(role.lastPayed); - const nextPayment = new Date( - lastPayment.setHours(lastPayment.getHours() + 1) - ); - if (new Date() < nextPayment) { - logger.silly(`Shop role ${roleId} is not due for payment.`); - } - const guildData = await guildSchema.findOne({ guildId }); - if (!guildData) { - logger.error(`Guild ${guildId} not found.`); - return; - } - if (!userId) { - logger.error(`User ID not found for shop role ${roleId}.`); - return; - } - const userData = await userSchema.findOne({ guildId, userId }); - if (!userData) { - logger.error(`User ${userId} not found for shop role ${roleId}.`); - return; - } - const rGuild = client?.guilds?.cache?.get(guildId); - const rMember = await rGuild?.members?.fetch(userId); - if (!rMember) { - logger.error(`Member ${userId} not found for shop role ${roleId}.`); - return; - } - const rRole = rMember.roles.cache.get(roleId); - if (!rMember || !rRole) { - logger.error(`Member ${userId} not found for shop role ${roleId}.`); - await shopRoleSchema - .deleteOne({ - userId, - roleId, - guildId, - }) - .then(async () => { - logger.silly( - `Shop role document ${roleId} has been deleted from user ${userId}.` - ); - }) - .catch(async (error) => { - logger.error( - `Error deleting shop role document ${roleId} from user ${userId}.`, - error - ); - }); - return; - } - if (new Date() > nextPayment) { - logger.silly( - `Shop role ${roleId} is due for payment. Withdrawing credits from user ${userId}.` - ); - const { pricePerHour } = guildData.shop.roles; - if (userData.credits < pricePerHour) { - logger.error( - `User ${userId} does not have enough credits to pay for shop role ${roleId}.` - ); - if (!rMember) { - logger.error(`Member ${userId} not found for shop role ${roleId}.`); - return; - } - rMember.roles.remove(roleId); - return; - } - userData.credits -= pricePerHour; - await userData - .save() - .then(async () => { - role.lastPayed = new Date(); - await role - .save() - .then(async () => { - logger.silly(`Shop role ${roleId} has been paid for.`); - }) - .catch(async (err) => { - logger.error( - `Error saving shop role ${roleId} last payed date.`, - err - ); - }); - logger.silly( - `Shop role ${roleId} has been paid for. Keeping role ${roleId} for user ${userId}.` - ); - }) - .catch(async (err) => { - logger.error( - `Error saving user ${userId} credits for shop role ${roleId}.`, - err - ); - }); - } - }) - ); -}; diff --git a/src/jobs/timeouts.ts b/src/jobs/timeouts/index.ts similarity index 84% rename from src/jobs/timeouts.ts rename to src/jobs/timeouts/index.ts index 3198772..ce18f09 100644 --- a/src/jobs/timeouts.ts +++ b/src/jobs/timeouts/index.ts @@ -1,8 +1,8 @@ -import logger from "../logger"; +import logger from "../../logger"; -import timeoutSchema from "../models/timeout"; +import timeoutSchema from "../../models/timeout"; -import addSeconds from "../helpers/addSeconds"; +import addSeconds from "../../helpers/addSeconds"; export const options = { schedule: "*/30 * * * *", // https://crontab.guru/ diff --git a/src/models/api.ts b/src/models/api.ts index d3690df..fd5cbd7 100644 --- a/src/models/api.ts +++ b/src/models/api.ts @@ -4,7 +4,7 @@ import { IEncryptionData } from "../interfaces/EncryptionData"; export interface IApi { guildId: Snowflake; - url: string; + url: IEncryptionData; token: IEncryptionData; } @@ -17,11 +17,18 @@ const apiSchema = new Schema( index: true, }, url: { - type: String, - required: true, - unique: false, - index: true, - default: "https://localhost/api/", + iv: { + type: String, + required: true, + unique: false, + index: true, + }, + content: { + type: String, + required: true, + unique: false, + index: true, + }, }, token: { iv: { @@ -29,14 +36,12 @@ const apiSchema = new Schema( required: true, unique: false, index: true, - default: "token", }, content: { type: String, required: true, unique: false, index: true, - default: "token", }, }, }, diff --git a/src/plugins/commands/credits/modules/top/index.ts b/src/plugins/commands/credits/modules/top/index.ts index 29aadda..54478aa 100644 --- a/src/plugins/commands/credits/modules/top/index.ts +++ b/src/plugins/commands/credits/modules/top/index.ts @@ -54,7 +54,7 @@ export default { embeds: [ embed .setDescription( - `Below are the top 10 users in this guild. + `Below are the top ten members in this guild. ${topTen.map(entry).join("\n")} ` diff --git a/src/plugins/commands/credits/modules/work/index.ts b/src/plugins/commands/credits/modules/work/index.ts index a84a696..ef0a550 100644 --- a/src/plugins/commands/credits/modules/work/index.ts +++ b/src/plugins/commands/credits/modules/work/index.ts @@ -45,7 +45,7 @@ export default { const guildDB = await fetchGuild(guild); - await cooldown.interaction(interaction, guildDB?.credits?.workTimeout); + await cooldown.command(interaction, guildDB?.credits?.workTimeout); const creditsEarned = chance.integer({ min: 0, diff --git a/src/plugins/commands/manage/modules/credits/modules/giveaway/index.ts b/src/plugins/commands/manage/modules/credits/modules/giveaway/index.ts index 9ee2f17..acb73cb 100644 --- a/src/plugins/commands/manage/modules/credits/modules/giveaway/index.ts +++ b/src/plugins/commands/manage/modules/credits/modules/giveaway/index.ts @@ -75,14 +75,16 @@ export default { if (!apiCredentials) return; + const url = encryption.decrypt(apiCredentials?.url); + const api = axios?.create({ - baseURL: `${apiCredentials?.url}/api/`, + baseURL: `${url}/api/`, headers: { Authorization: `Bearer ${encryption.decrypt(apiCredentials.token)}`, }, }); - const shopUrl = `${apiCredentials?.url}/store`; + const shopUrl = `${url}/store`; await api .post("vouchers", { diff --git a/src/plugins/commands/moderation/index.ts b/src/plugins/commands/moderation/index.ts new file mode 100644 index 0000000..73b4994 --- /dev/null +++ b/src/plugins/commands/moderation/index.ts @@ -0,0 +1,22 @@ +import { SlashCommandBuilder } from "@discordjs/builders"; +import { CommandInteraction } from "discord.js"; + +import modules from "./modules"; +export const moduleData = modules; + +export const builder = new SlashCommandBuilder() + .setName("moderation") + .setDescription("Moderation.") + + .addSubcommand(modules.prune.builder); + +export const execute = async (interaction: CommandInteraction) => { + switch (interaction.options.getSubcommand()) { + case "prune": + return modules.prune.execute(interaction); + default: + throw new Error( + `Unknown subcommand: ${interaction.options.getSubcommand()}` + ); + } +}; diff --git a/src/plugins/commands/moderation/modules/index.ts b/src/plugins/commands/moderation/modules/index.ts new file mode 100644 index 0000000..e7dd532 --- /dev/null +++ b/src/plugins/commands/moderation/modules/index.ts @@ -0,0 +1,3 @@ +import prune from "./prune"; + +export default { prune }; diff --git a/src/plugins/commands/moderation/modules/prune/index.ts b/src/plugins/commands/moderation/modules/prune/index.ts new file mode 100644 index 0000000..c847e47 --- /dev/null +++ b/src/plugins/commands/moderation/modules/prune/index.ts @@ -0,0 +1,91 @@ +// Dependencies +import { + CommandInteraction, + Permissions, +} from "discord.js"; + +// Configurations +import getEmbedConfig from "../../../../../helpers/getEmbedConfig"; + +import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; + +// Function +export default { + metadata: { + guildOnly: true, + ephemeral: false, + permissions: [Permissions.FLAGS.MANAGE_MESSAGES], + }, + + builder: (command: SlashCommandSubcommandBuilder) => { + return command + .setName("prune") + .setDescription("Prune messages!") + .addIntegerOption((option) => + option + .setName("count") + .setDescription("How many messages you want to prune.") + .setRequired(true) + ) + .addBooleanOption((option) => + option.setName("bots").setDescription("Include bots.") + ); + }, + execute: async (interaction: CommandInteraction) => { + const { successColor, footerText, footerIcon } = await getEmbedConfig( + interaction.guild + ); + + const count = interaction.options.getInteger("count"); + if (count == null) return; + const bots = interaction.options.getBoolean("bots"); + + if (count < 1 || count > 100) { + const interactionEmbed = { + title: "[:police_car:] Prune", + description: `You can only prune between 1 and 100 messages.`, + color: successColor, + timestamp: new Date(), + footer: { + iconURL: footerIcon, + text: footerText, + }, + }; + await interaction.editReply({ + embeds: [interactionEmbed], + }); + return; + } + + if (interaction?.channel?.type !== "GUILD_TEXT") return; + await interaction.channel.messages.fetch().then(async (messages) => { + const messagesToDelete = ( + bots + ? messages.filter((m) => m?.interaction?.id !== interaction.id) + : messages.filter( + (m) => + m?.interaction?.id !== interaction.id && m?.author?.bot !== true + ) + ).first(count); + + if (interaction?.channel?.type !== "GUILD_TEXT") return; + await interaction.channel + .bulkDelete(messagesToDelete, true) + .then(async () => { + const interactionEmbed = { + title: "[:police_car:] Prune", + description: `Successfully pruned \`${count}\` messages.`, + color: successColor, + timestamp: new Date(), + footer: { + iconURL: footerIcon, + text: footerText, + }, + }; + await interaction.editReply({ + embeds: [interactionEmbed], + }); + }); + }); + }, +}; diff --git a/src/plugins/commands/reputation/modules/give/index.ts b/src/plugins/commands/reputation/modules/give/index.ts index c899ecb..eb58abe 100644 --- a/src/plugins/commands/reputation/modules/give/index.ts +++ b/src/plugins/commands/reputation/modules/give/index.ts @@ -52,7 +52,7 @@ export default { await noSelfReputation(optionTarget, user); // Check if user is on cooldown otherwise create one - await cooldown.interaction(interaction, timeout); + await cooldown.command(interaction, timeout); switch (optionType) { case "positive": diff --git a/src/plugins/commands/shop/modules/cpgg/index.ts b/src/plugins/commands/shop/modules/cpgg/index.ts index aeee472..90dbe70 100644 --- a/src/plugins/commands/shop/modules/cpgg/index.ts +++ b/src/plugins/commands/shop/modules/cpgg/index.ts @@ -29,6 +29,7 @@ export default { option .setName("amount") .setDescription("How much credits you want to withdraw.") + .setRequired(true) ); }, execute: async (interaction: CommandInteraction) => { @@ -152,15 +153,16 @@ export default { }); if (!apiCredentials) return; + const url = encryption.decrypt(apiCredentials?.url); const api = axios?.create({ - baseURL: `${apiCredentials.url}/api/`, + baseURL: `${url}/api/`, headers: { Authorization: `Bearer ${encryption.decrypt(apiCredentials.token)}`, }, }); - const shopUrl = `${apiCredentials?.url}/store`; + const shopUrl = `${url}/store`; const buttons = new MessageActionRow().addComponents( new MessageButton() diff --git a/src/plugins/commands/utility/modules/about/index.ts b/src/plugins/commands/utility/modules/about/index.ts index f89d74d..92af9cb 100644 --- a/src/plugins/commands/utility/modules/about/index.ts +++ b/src/plugins/commands/utility/modules/about/index.ts @@ -29,6 +29,11 @@ export default { .setStyle("LINK") .setEmoji("📄") .setURL("https://github.com/ZynerOrg/xyter"), + new MessageButton() + .setLabel("Documentation") + .setStyle("LINK") + .setEmoji("📚") + .setURL("https://xyter.zyner.org"), new MessageButton() .setLabel("Website") .setStyle("LINK") diff --git a/src/plugins/events/interactionCreate/components/checks.ts b/src/plugins/events/interactionCreate/components/checks.ts deleted file mode 100644 index 006a14f..0000000 --- a/src/plugins/events/interactionCreate/components/checks.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CommandInteraction, MessageEmbed } from "discord.js"; -import * as cooldown from "../../../../helpers/cooldown"; -import logger from "../../../../logger"; - -export default async ( - interaction: CommandInteraction, - metadata: any, - embedConfig: any -) => { - if ( - metadata.permissions && - metadata.guildOnly && - !interaction.memberPermissions?.has(metadata.permissions) - ) { - return interaction?.editReply({ - embeds: [ - new MessageEmbed() - .setTitle("[:x:] Permission") - .setDescription(`You do not have the permission to manage the bot.`) - .setTimestamp(new Date()) - .setColor(embedConfig.errorColor) - .setFooter({ - text: embedConfig.footerText, - iconURL: embedConfig.footerIcon, - }), - ], - }); - } - - logger.info(metadata); - - if (metadata.cooldown) { - await cooldown - .interaction(interaction, metadata.cooldown) - .catch(async (error) => { - throw new Error("Cooldown error: " + error); - }); - } - - if (metadata.guildOnly) { - if (!interaction.guild) { - throw new Error("This command is guild only."); - } - } - - if (metadata.dmOnly) { - if (interaction.guild) { - throw new Error("This command is DM only."); - } - } -}; diff --git a/src/plugins/events/interactionCreate/components/isButton.ts b/src/plugins/events/interactionCreate/components/isButton.ts deleted file mode 100644 index eaf1605..0000000 --- a/src/plugins/events/interactionCreate/components/isButton.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Dependencies -import { CommandInteraction, MessageEmbed } from "discord.js"; - -import logger from "../../../../logger"; - -import deferReply from "../../../../helpers/deferReply"; -import getEmbedConfig from "../../../../helpers/getEmbedConfig"; -import capitalizeFirstLetter from "../../../../helpers/capitalizeFirstLetter"; -import * as cooldown from "../../../../helpers/cooldown"; - -export default async (interaction: CommandInteraction) => { - if (!interaction.isButton()) return; - - const { errorColor, footerText, footerIcon } = await getEmbedConfig( - interaction.guild - ); - - const { guild, customId, user, memberPermissions } = interaction; - - const currentButton = await import(`../../../buttons/${customId}`); - - if (currentButton == null) { - logger.silly(`Button ${customId} not found`); - } - - const metadata = currentButton.metadata; - - await deferReply(interaction, metadata.ephemeral || false); - - if (metadata.guildOnly) { - if (!guild) { - logger.debug(`Guild is null`); - - return interaction.editReply({ - embeds: [ - new MessageEmbed() - .setTitle("[:x:] Permission") - .setDescription("This command is only available for guild") - .setColor(errorColor) - .setTimestamp(new Date()) - .setFooter({ text: footerText, iconURL: footerIcon }), - ], - }); - } - } - - if ( - metadata.permissions && - metadata.guildOnly && - !memberPermissions?.has(metadata.permissions) - ) { - return interaction?.editReply({ - embeds: [ - new MessageEmbed() - .setTitle("[:x:] Permission") - .setDescription(`You do not have the permission to manage the bot.`) - .setTimestamp(new Date()) - .setColor(errorColor) - .setFooter({ text: footerText, iconURL: footerIcon }), - ], - }); - } - - if (metadata.dmOnly) { - if (guild) { - logger.silly(`Guild exist`); - - return interaction.editReply({ - embeds: [ - new MessageEmbed() - .setTitle("[:x:] Permission") - .setDescription("This command is only available in DM.") - .setColor(errorColor) - .setTimestamp(new Date()) - .setFooter({ text: footerText, iconURL: footerIcon }), - ], - }); - } - } - - if (metadata.cooldown) { - await cooldown - .interaction(interaction, metadata.cooldown) - .catch(async (error) => { - return interaction?.editReply({ - embeds: [ - new MessageEmbed() - .setTitle("[:x:] Permission") - .setDescription(`${error}`) - .setTimestamp(new Date()) - .setColor(errorColor) - .setFooter({ text: footerText, iconURL: footerIcon }), - ], - }); - }); - } - - await currentButton - .execute(interaction) - .then(async () => { - return logger?.silly( - `Button: ${customId} executed in guild: ${guild?.name} (${guild?.id}) by user: ${user?.tag} (${user?.id})` - ); - }) - .catch(async (error: string) => { - logger?.debug(`INTERACTION BUTTON CATCH: ${error}`); - - return interaction.editReply({ - embeds: [ - new MessageEmbed() - .setTitle( - `[:x:] ${capitalizeFirstLetter( - interaction.options.getSubcommand() - )}` - ) - .setDescription(`${"``"}${error}${"``"}`) - .setColor(errorColor) - .setTimestamp(new Date()) - .setFooter({ text: footerText, iconURL: footerIcon }), - ], - }); - }); -}; diff --git a/src/plugins/events/interactionCreate/components/isCommand.ts b/src/plugins/events/interactionCreate/components/isCommand.ts deleted file mode 100644 index 3a14ad6..0000000 --- a/src/plugins/events/interactionCreate/components/isCommand.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Dependencies -import { CommandInteraction, MessageEmbed } from "discord.js"; - -import logger from "../../../../logger"; - -import deferReply from "../../../../helpers/deferReply"; -import getEmbedConfig from "../../../../helpers/getEmbedConfig"; -import getCommandMetadata from "../../../../helpers/getCommandMetadata"; -import capitalizeFirstLetter from "../../../../helpers/capitalizeFirstLetter"; -import * as cooldown from "../../../../helpers/cooldown"; - -export default async (interaction: CommandInteraction) => { - if (!interaction.isCommand()) return; - - const { errorColor, footerText, footerIcon } = await getEmbedConfig( - interaction.guild - ); - - const { client, guild, commandName, user, memberPermissions } = interaction; - - const currentCommand = client.commands.get(commandName); - - if (currentCommand == null) { - logger.silly(`Command ${commandName} not found`); - } - - const metadata = await getCommandMetadata(interaction, currentCommand); - - await deferReply(interaction, metadata.ephemeral || false); - - if (metadata.guildOnly && !guild) - throw new Error("This command is guild only."); - - if ( - metadata.permissions && - metadata.guildOnly && - !memberPermissions?.has(metadata.permissions) - ) - throw new Error("You don't have the required permissions"); - - if (metadata.dmOnly && guild) - throw new Error("This command is only available in DM"); - - if (metadata.cooldown) - await cooldown.interaction(interaction, metadata.cooldown); - - await currentCommand.execute(interaction); -}; diff --git a/src/plugins/events/interactionCreate/handlers/button/index.ts b/src/plugins/events/interactionCreate/handlers/button/index.ts new file mode 100644 index 0000000..3373f09 --- /dev/null +++ b/src/plugins/events/interactionCreate/handlers/button/index.ts @@ -0,0 +1,36 @@ +// Dependencies +import { Interaction } from "discord.js"; + +import deferReply from "../../../../../helpers/deferReply"; +import * as cooldown from "../../../../../helpers/cooldown"; + +export default async (interaction: Interaction) => { + if (!interaction.isButton()) return; + + const { guild, customId, memberPermissions } = interaction; + + const currentButton = await import(`../../../buttons/${customId}`); + + if (!currentButton) throw new Error(`Unknown button ${customId}`); + + const metadata = currentButton.metadata; + + await deferReply(interaction, metadata.ephemeral || false); + + if (metadata.guildOnly && !guild) + throw new Error("This command is guild only."); + + if ( + metadata.permissions && + metadata.guildOnly && + !memberPermissions?.has(metadata.permissions) + ) + throw new Error("You don't have the required permissions"); + + if (metadata.dmOnly && guild) + throw new Error("This command is only available in DM"); + + if (metadata.cooldown) await cooldown.button(interaction, metadata.cooldown); + + await currentButton.execute(interaction); +}; diff --git a/src/plugins/events/interactionCreate/handlers/command/index.ts b/src/plugins/events/interactionCreate/handlers/command/index.ts new file mode 100644 index 0000000..b6b30ec --- /dev/null +++ b/src/plugins/events/interactionCreate/handlers/command/index.ts @@ -0,0 +1,34 @@ +// Dependencies +import { Interaction } from "discord.js"; + +import deferReply from "../../../../../helpers/deferReply"; +import getCommandMetadata from "../../../../../helpers/getCommandMetadata"; +import * as cooldown from "../../../../../helpers/cooldown"; + +export default async (interaction: Interaction) => { + if (!interaction.isCommand()) return; + const { client, commandName } = interaction; + + const currentCommand = client.commands.get(commandName); + if (!currentCommand) throw new Error(`Unknown command ${commandName}`); + + const metadata = await getCommandMetadata(interaction, currentCommand); + await deferReply(interaction, metadata.ephemeral || false); + + if (metadata.guildOnly && !interaction.guild) + throw new Error("This command is guild only."); + + if ( + metadata.permissions && + metadata.guildOnly && + !interaction.memberPermissions?.has(metadata.permissions) + ) + throw new Error("You don't have the required permissions"); + + if (metadata.dmOnly && interaction.guild) + throw new Error("This command is only available in DM"); + + if (metadata.cooldown) await cooldown.command(interaction, metadata.cooldown); + + await currentCommand.execute(interaction); +}; diff --git a/src/plugins/events/interactionCreate/handlers/index.ts b/src/plugins/events/interactionCreate/handlers/index.ts new file mode 100644 index 0000000..c29bfc3 --- /dev/null +++ b/src/plugins/events/interactionCreate/handlers/index.ts @@ -0,0 +1,11 @@ +import { Interaction } from "discord.js"; + +import button from "./button"; +import command from "./command"; + +import logger from "../../../../logger"; + +export const execute = async (interaction: Interaction) => { + await button(interaction); + await command(interaction); +}; diff --git a/src/plugins/events/interactionCreate/index.ts b/src/plugins/events/interactionCreate/index.ts index f7365ec..aa6e763 100644 --- a/src/plugins/events/interactionCreate/index.ts +++ b/src/plugins/events/interactionCreate/index.ts @@ -2,8 +2,8 @@ import { CommandInteraction, MessageEmbed } from "discord.js"; // Dependencies -import isCommand from "../../events/interactionCreate/components/isCommand"; -import isButton from "../../events/interactionCreate/components/isButton"; +import * as handlers from "./handlers"; + import logger from "../../../logger"; import audits from "./audits"; import { IEventOptions } from "../../../interfaces/EventOptions"; @@ -27,11 +27,8 @@ export const execute = async (interaction: CommandInteraction) => { await audits.execute(interaction); - try { - await isCommand(interaction); - await isButton(interaction); - } catch (error) { - logger.debug(`${error}`); + await handlers.execute(interaction).catch(async (err) => { + logger.debug(`${err}`); return interaction.editReply({ embeds: [ @@ -41,11 +38,11 @@ export const execute = async (interaction: CommandInteraction) => { interaction.options.getSubcommand() )}` ) - .setDescription(`${"``"}${error}${"``"}`) + .setDescription(`${"``"}${err}${"``"}`) .setColor(errorColor) .setTimestamp(new Date()) .setFooter({ text: footerText, iconURL: footerIcon }), ], }); - } + }); };