diff --git a/JavaScript/cooldown.js b/JavaScript/cooldown.js new file mode 100644 index 0000000..ea254be --- /dev/null +++ b/JavaScript/cooldown.js @@ -0,0 +1,137 @@ +// @ts-nocheck + +/** + * Allows you to set cooldowns (or "ratelimits") for commands + * + * @author @HighArcs + * @version 1.0.0 + * @example + * ```ts + * import { cooldown } from "../plugins/cooldown"; + * import { sernModule, CommandType } from "@sern/handler"; + * export default commandModule({ + * plugins: [cooldown.add( [ ['channel', '1/4'] ] )], // limit to 1 action every 4 seconds per channel + * execute: (ctx) => {} + * }) + * ``` + */ +import { PluginType } from "@sern/handler"; +import { GuildMember } from "discord.js"; +/** + * actions/seconds + */ + +export let CooldownLocation; + +(function (CooldownLocation) { + CooldownLocation["channel"] = "channel"; + CooldownLocation["user"] = "user"; + CooldownLocation["guild"] = "guild"; +})(CooldownLocation || (CooldownLocation = {})); + +export class ExpiryMap extends Map { + constructor(expiry = Infinity, iterable = []) { + super(iterable); + this.expiry = expiry; + } + + set(key, value, expiry = this.expiry) { + super.set(key, value); + if (expiry !== Infinity) + setTimeout(() => { + super.delete(key); + }, expiry); + return this; + } +} +export const map = new ExpiryMap(); + +function parseCooldown(location, cooldown) { + const [actions, seconds] = cooldown.split("/").map((s) => Number(s)); + + if ( + !Number.isSafeInteger(actions) || + !Number.isSafeInteger(seconds) || + actions === undefined || + seconds === undefined + ) { + throw new Error(`Invalid cooldown string: ${cooldown}`); + } + + return { + actions, + seconds, + location, + }; +} + +function getPropertyForLocation(context, location) { + switch (location) { + case CooldownLocation.channel: + return context.channel.id; + + case CooldownLocation.user: + if (!context.member || !(context.member instanceof GuildMember)) { + throw new Error("context.member is not a GuildMember"); + } + + return context.member.id; + + case CooldownLocation.guild: + return context.guildId; + } +} + +function add(items, message) { + const raw = items.map((c) => { + if (!Array.isArray(c)) return c; + return parseCooldown(c[0], c[1]); + }); + return { + name: "cooldown", + description: "limits user/channel/guild actions", + type: PluginType.Event, + + async execute([context], controller) { + for (const { location, actions, seconds } of raw) { + const id = getPropertyForLocation(context, location); + const cooldown = map.get(id); + + if (!cooldown) { + map.set(id, 1, seconds * 1000); + continue; + } + + if (cooldown >= actions) { + if (message) { + await message({ + location, + actions: cooldown, + maxActions: actions, + seconds, + context, + }); + } + + return controller.stop(); + } + + map.set(id, cooldown + 1, seconds * 1000); + } + + return controller.next(); + }, + }; +} + +const locations = { + [CooldownLocation.channel]: (value) => + add([[CooldownLocation.channel, value]]), + [CooldownLocation.user]: (value) => add([[CooldownLocation.user, value]]), + [CooldownLocation.guild]: (value) => add([[CooldownLocation.guild, value]]), +}; +export const cooldown = { + add, + locations, + map, +}; diff --git a/JavaScript/dmOnly.js b/JavaScript/dmOnly.js new file mode 100644 index 0000000..5ec0571 --- /dev/null +++ b/JavaScript/dmOnly.js @@ -0,0 +1,37 @@ +// @ts-nocheck + +/** + * This is dmOnly plugin, it allows commands to be run only in DMs. + * + * @author @EvolutionX-10 + * @version 1.0.0 + * @requires `partials: [Partials.Channel], intents: [GatewayIntentBits.DirectMessages, GatewayIntentBits.MessageContent] + * @example + * ```ts + * import { dmOnly } from "../path/to/your/plugin/folder"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [dmOnly()], + * execute: // your code + * }) + * ``` + */ +import { PluginType } from "@sern/handler"; +export function dmOnly(content, ephemeral) { + return { + type: PluginType.Event, + description: "Allows commands to be run in DM only", + + async execute(event, controller) { + const [ctx] = event; + if (ctx.channel?.isDMBased()) return controller.next(); + if (content) + await ctx.reply({ + content, + ephemeral, + }); // Change this if you want or remove it for silent deny + + return controller.stop(); + }, + }; +} diff --git a/JavaScript/nsfwOnly.js b/JavaScript/nsfwOnly.js new file mode 100644 index 0000000..426f617 --- /dev/null +++ b/JavaScript/nsfwOnly.js @@ -0,0 +1,66 @@ +//@ts-nocheck + +/** + * This plugin checks if the channel is nsfw and responds to user with a specified response if not nsfw + * + * @author @NeoYaBoi + * @version 1.0.0 + * @example + * ```ts + * import { nsfwOnly } from "../plugins/nsfwOnly"; //(change if need be) + * import { sernModule, CommandType } from "@sern/handler"; + * export default commandModule({ + * plugins: [ nsfwOnly('response', true/false) ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ +import { ChannelType } from "discord.js"; +import { PluginType } from "@sern/handler"; + +function isGuildText(channel) { + return ( + channel?.type == ChannelType.GuildPublicThread || + channel?.type == ChannelType.GuildPrivateThread + ); +} + +export function nsfwOnly(onFail, ephemeral) { + return { + type: PluginType.Event, + description: "Checks if the channel is nsfw or not.", + + async execute(event, controller) { + const [ctx] = event; //checking if command was executed in dms + + if (ctx.guild === null) { + await ctx.reply({ + content: onFail, + ephemeral, + }); + return controller.stop(); + } //channel is thread (not supported by nsfw) + + if (isGuildText(ctx.channel) == true) { + await ctx.reply({ + content: onFail, + ephemeral, + }); + return controller.stop(); + } + + if (!ctx.channel.nsfw) { + //channel is not nsfw + await ctx.reply({ + content: onFail, + ephemeral, + }); + return controller.stop(); + } //continues to command if nsfw + + return controller.next(); + }, + }; +} diff --git a/JavaScript/ownerOnly.js b/JavaScript/ownerOnly.js new file mode 100644 index 0000000..bb14335 --- /dev/null +++ b/JavaScript/ownerOnly.js @@ -0,0 +1,33 @@ +// @ts-nocheck + +/** + * This is OwnerOnly plugin, it allows only bot owners to run the command, like eval. + * + * @author @EvolutionX-10 + * @version 1.0.0 + * @example + * ```ts + * import { ownerOnly } from "../path/to/your/plugin/folder"; + * import { sernModule, CommandType } from "@sern/handler"; + * export default sernModule([OwnerOnly()], { + * // your code + * }) + * ``` + */ +import { PluginType } from "@sern/handler"; +const ownerIDs = ["697795666373640213"]; //! Fill your ID + +export function ownerOnly() { + return { + type: PluginType.Event, + description: "Allows only bot owner to run command", + + async execute(event, controller) { + const [ctx] = event; + if (ownerIDs.includes(ctx.user.id)) return controller.next(); //* If you want to reply when the command fails due to user not being owner, you can use following + // await ctx.reply("Only owner can run it!!!"); + + return controller.stop(); //! Important: It stops the execution of command! + }, + }; +} diff --git a/JavaScript/permCheck.js b/JavaScript/permCheck.js new file mode 100644 index 0000000..4c8c33b --- /dev/null +++ b/JavaScript/permCheck.js @@ -0,0 +1,46 @@ +// @ts-nocheck + +/** + * This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check user for that perm). + * + * @author @NeoYaBoi + * @version 1.0.1 + * @example + * ```ts + * import { permCheck } from "../plugins/permCheck"; //(change if need be) + * import { sernModule, CommandType } from "@sern/handler"; + * export default commandModule({ + * plugins: [ permCheck('permission', 'No permission response') ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ +import { PluginType } from "@sern/handler"; +export function permCheck(perm, response) { + return { + type: PluginType.Event, + description: "Checks for specified perm", + + async execute(event, controller) { + const [ctx] = event; + + if (ctx.guild === null) { + ctx.reply("This command cannot be used here"); + console.warn( + "PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)" + ); //delete this line if you dont want to be notified when a command is used outside of a guild/server + + return controller.stop(); + } + + if (!ctx.member.permissions.has(perm)) { + await ctx.reply(response); + return controller.stop(); + } + + return controller.next(); + }, + }; +} diff --git a/JavaScript/publish.js b/JavaScript/publish.js new file mode 100644 index 0000000..93113ed --- /dev/null +++ b/JavaScript/publish.js @@ -0,0 +1,116 @@ +// @ts-nocheck + +/** + * This is publish plugin, it allows you to publish your slash commands with ease. + * + * @author @EvolutionX-10 + * @version 1.1.0 + * @example + * ```ts + * import { publish } from "../path/to/your/plugin/folder"; + * import { sernModule, CommandType } from "@sern/handler"; + * export default sernModule([publish()], { // put guild id in array for guild commands + * // your code + * }) + * ``` + */ +import { CommandType, PluginType } from "@sern/handler"; +import { ApplicationCommandType } from "discord.js"; +export function publish(guildIds = []) { + return { + type: PluginType.Command, + description: "Manage Slash Commands", + name: "slash-auto-publish", + + async execute({ client }, { absPath, mod: module }, controller) { + function c(e) { + console.error("publish command didnt work for", module.name); + console.error(e); + } + + try { + const commandData = { + type: CommandTypeRaw[module.type], + name: module.name, + description: module.description, + options: optionsTransformer(module.options ?? []), + }; + if (!Array.isArray(guildIds)) guildIds = [guildIds]; + + if (!guildIds.length) { + const cmd = ( + await client.application.commands.fetch() + ).find((c) => c.name === module.name); + + if (cmd) { + if (!cmd.equals(commandData, true)) { + console.log( + `Found differences in global command ${module.name}` + ); + await cmd.edit(commandData).then((c) => { + console.log( + `${module.name} updated with new data successfully!` + ); + }); + } + + return controller.next(); + } + + await client.application.commands + .create(commandData) + .catch(c); + console.log("Command created", module.name); + return controller.next(); + } + + for (const id of guildIds) { + const guild = await client.guilds.fetch(id).catch(c); + if (!guild) continue; + const guildcmd = (await guild.commands.fetch()).find( + (c) => c.name === module.name + ); + + if (guildcmd) { + if (!guildcmd.equals(commandData, true)) { + console.log( + `Found differences in command ${module.name}` + ); + await guildcmd.edit(commandData).catch(c); + console.log( + `${module.name} updated with new data successfully!` + ); + continue; + } + + continue; + } + + await guild.commands.create(commandData).catch(c); + console.log( + "Guild Command created", + module.name, + guild.name + ); + } + + return controller.next(); + } catch (e) { + console.log("Command did not register" + module.name); + console.log(e); + return controller.stop(); + } + }, + }; +} +export function optionsTransformer(ops) { + return ops.map((el) => + el.autocomplete ? (({ command, ...el }) => el)(el) : el + ); +} +export const CommandTypeRaw = { + [CommandType.Both]: ApplicationCommandType.ChatInput, + [CommandType.MenuMsg]: ApplicationCommandType.Message, + [CommandType.MenuUser]: ApplicationCommandType.User, + [CommandType.Slash]: ApplicationCommandType.ChatInput, +};