From ce0bd4bef2399d5548e0e685e11f04df5cb829d9 Mon Sep 17 00:00:00 2001 From: EvolutionX Date: Sat, 6 Aug 2022 13:00:58 +0530 Subject: [PATCH 1/3] feat: plugin command --- package-lock.json | 35 ++++++--- package.json | 7 +- src/commands/eval.ts | 19 ++++- src/commands/plugin.ts | 112 ++++++++++++++++++++++++++++ src/index.ts | 19 +++++ src/plugins/cooldown.ts | 144 ++++++++++++++++++++++++++++++++++++ src/plugins/refreshCache.ts | 22 ++++++ 7 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 src/commands/plugin.ts create mode 100644 src/plugins/cooldown.ts create mode 100644 src/plugins/refreshCache.ts diff --git a/package-lock.json b/package-lock.json index dd745e4..a441174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,15 @@ "@sern/handler": "^1.1.9-beta", "discord.js": "^14.0.3", "dotenv": "^16.0.1", + "jsdoc-parse-plus": "^1.3.0", "string-similarity": "^4.0.4", - "trie-search": "^1.3.6" + "trie-search": "^1.3.6", + "undici": "^5.8.1" }, "devDependencies": { "@types/node": "^17.0.25", "@types/string-similarity": "^4.0.0", - "tsup": "^6.2.1", - "typescript": "^4.6.3" + "tsup": "^6.2.1" } }, "node_modules/@discordjs/builders": { @@ -1045,6 +1046,11 @@ "node": ">=10" } }, + "node_modules/jsdoc-parse-plus": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsdoc-parse-plus/-/jsdoc-parse-plus-1.3.0.tgz", + "integrity": "sha512-zk1ssDQX8C2wLf6Gd6RdLr/Ou+E98fB2YlWZP7t3CLkX/4ULeg6afESLdAMdsKNeAO5lmSi4tbGf6o4xloPGew==" + }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -1705,6 +1711,8 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true, + "optional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1714,9 +1722,9 @@ } }, "node_modules/undici": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.8.0.tgz", - "integrity": "sha512-1F7Vtcez5w/LwH2G2tGnFIihuWUlc58YidwLiCv+jR2Z50x0tNXpRRw7eOIJ+GvqCqIkg9SB7NWAJ/T9TLfv8Q==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.8.1.tgz", + "integrity": "sha512-iDRmWX4Zar/4A/t+1LrKQRm102zw2l9Wgat3LtTlTn8ykvMZmAmpq9tjyHEigx18FsY7IfATvyN3xSw9BDz0eA==", "engines": { "node": ">=12.18" } @@ -2447,6 +2455,11 @@ "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", "dev": true }, + "jsdoc-parse-plus": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsdoc-parse-plus/-/jsdoc-parse-plus-1.3.0.tgz", + "integrity": "sha512-zk1ssDQX8C2wLf6Gd6RdLr/Ou+E98fB2YlWZP7t3CLkX/4ULeg6afESLdAMdsKNeAO5lmSi4tbGf6o4xloPGew==" + }, "lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -2890,12 +2903,14 @@ "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "undici": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.8.0.tgz", - "integrity": "sha512-1F7Vtcez5w/LwH2G2tGnFIihuWUlc58YidwLiCv+jR2Z50x0tNXpRRw7eOIJ+GvqCqIkg9SB7NWAJ/T9TLfv8Q==" + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.8.1.tgz", + "integrity": "sha512-iDRmWX4Zar/4A/t+1LrKQRm102zw2l9Wgat3LtTlTn8ykvMZmAmpq9tjyHEigx18FsY7IfATvyN3xSw9BDz0eA==" }, "util-deprecate": { "version": "1.0.2", diff --git a/package.json b/package.json index a5ba162..49f5a8d 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,14 @@ "@sern/handler": "^1.1.9-beta", "discord.js": "^14.0.3", "dotenv": "^16.0.1", + "jsdoc-parse-plus": "^1.3.0", "string-similarity": "^4.0.4", - "trie-search": "^1.3.6" + "trie-search": "^1.3.6", + "undici": "^5.8.1" }, "devDependencies": { "@types/node": "^17.0.25", "@types/string-similarity": "^4.0.0", - "tsup": "^6.2.1", - "typescript": "^4.6.3" + "tsup": "^6.2.1" } } diff --git a/src/commands/eval.ts b/src/commands/eval.ts index 400f63e..2790fcc 100644 --- a/src/commands/eval.ts +++ b/src/commands/eval.ts @@ -1,6 +1,9 @@ -import { Args, CommandType, Context, commandModule } from "@sern/handler"; +import { CommandType, commandModule, Context } from "@sern/handler"; +import { Client, Collection } from "discord.js"; import { inspect } from "util"; +import { fetch } from "undici"; import { ownerOnly } from "../plugins/ownerOnly"; +import type { Data } from "./plugin"; export default commandModule({ type: CommandType.Text, @@ -46,3 +49,17 @@ export default commandModule({ ctx.channel!.send({ content: result as string }); }, }); + +export async function cp(client: Client) { + const cache: Collection = new Collection(); + const link = `https://api.github.com/repos/sern-handler/awesome-plugins/contents/TypeScript`; + const resp = await fetch(link).catch(() => null); + if (!resp) return null; + const dataArray = (await resp.json()) as Data[]; + for (const data of dataArray) { + const name = data.name.replace(".ts", ""); + cache.set(name, data); + } + client.cache = cache; + return cache; +} diff --git a/src/commands/plugin.ts b/src/commands/plugin.ts new file mode 100644 index 0000000..f48114c --- /dev/null +++ b/src/commands/plugin.ts @@ -0,0 +1,112 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { publish } from "../plugins/publish"; +import { fetch } from "undici"; +import { ApplicationCommandOptionType, EmbedBuilder } from "discord.js"; +import { cooldown } from "../plugins/cooldown"; +import { parse } from "jsdoc-parse-plus"; +import { refreshCache } from "../plugins/refreshCache"; +export default commandModule({ + type: CommandType.Slash, + description: "View sern plugins", + options: [ + { + name: "plugin", + description: "The plugin to view", + type: ApplicationCommandOptionType.String, + required: true, + autocomplete: true, + command: { + onEvent: [], + async execute(ctx) { + const { cache } = ctx.client; + const focus = ctx.options.getFocused(); + if (!cache) + return ctx.respond([{ name: "No plugins found", value: "" }]); + const data = [...cache.values()] as Data[]; + + const plugins = data.map((d) => { + const name = d.name.replace(".ts", ""); + return { name, value: d.download_url }; + }); + return ctx.respond( + plugins.filter((p) => + p.name.toLowerCase().includes(focus?.toLowerCase()) + ) + ); + }, + }, + }, + ], + plugins: [ + refreshCache(), + publish(), + cooldown.add([["user", "1/10"]], ({ seconds, context }) => + context.reply({ + content: `You gotta chill for ${seconds} seconds`, + ephemeral: true, + }) + ), + ], + async execute(ctx, [, options]) { + const url = options.getString("plugin", true) as string; + const name = ctx.client.cache?.findKey((d) => d.download_url === url); + let data = await fetch(url, { method: "GET" }) + .then((r) => r.text()) + .catch(() => null); + if (!data || !name) + return ctx.reply(`No plugin found at this [link](${url})`); + + const JSdoc = parse(data) as A; + const github = `https://github.com/sern-handler/awesome-plugins/blob/main/TypeScript/${name}.ts`; + + const embed = new EmbedBuilder() + .setColor("Random") + .setTimestamp() + .setTitle(`${name}`) + .setURL(github) + .setFields( + { + name: "Description", + value: JSdoc.description.value, + }, + { + name: "Version", + value: JSdoc.version.value, + }, + { + name: "Author", + value: JSdoc.author.value, + }, + { + name: "Example", + value: (JSdoc as unknown as B).example[0].value, + } + ); + + return ctx.reply({ + embeds: [embed], + }); + }, +}); + +export interface Data { + name: string; + download_url: string; +} + +interface ParsedData { + author: DocData; + description: DocData; + version: DocData; + example: DocData[]; + requires?: DocData[]; +} + +interface DocData { + tag: string; + value: string; + raw: string; +} + +type A = Record; +type B = Record; diff --git a/src/index.ts b/src/index.ts index dec3679..375be58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,3 +30,22 @@ client.once("ready", (client) => { }); client.login(); +// const d = `// @ts-nocheck +// /** +// * This is dmOnly plugin, it allows commands to be run only in DMs. +// * +// * @author @EvolutionX-10 +// * @version 1.1.0-beta +// * @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 +// * }) +// * \`\`\` +// */` + +// console.log(parse(d, ['@license'])); \ No newline at end of file diff --git a/src/plugins/cooldown.ts b/src/plugins/cooldown.ts new file mode 100644 index 0000000..3adf2f0 --- /dev/null +++ b/src/plugins/cooldown.ts @@ -0,0 +1,144 @@ +import { CommandType, Context, EventPlugin, PluginType } from "@sern/handler"; +import { GuildMember } from "discord.js"; +/** + * actions/seconds + */ +export type CooldownString = `${number}/${number}`; +export interface Cooldown { + location: CooldownLocation; + seconds: number; + actions: number; +} +export enum CooldownLocation { + channel = "channel", + user = "user", + guild = "guild", +} + +export class ExpiryMap extends Map { + public readonly expiry: number; + constructor( + expiry: number = Infinity, + iterable: [K, V][] | ReadonlyMap = [] + ) { + super(iterable); + this.expiry = expiry; + } + + public set(key: K, value: V, expiry: number = this.expiry): this { + super.set(key, value); + if (expiry !== Infinity) + setTimeout(() => { + super.delete(key); + }, expiry); + return this; + } +} + +export const map = new ExpiryMap(); + +function parseCooldown( + location: CooldownLocation, + cooldown: CooldownString +): 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: Context, location: CooldownLocation) { + 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; + } +} + +export interface RecievedCooldown { + location: CooldownLocation; + actions: number; + maxActions: number; + seconds: number; + context: Context; +} +type CooldownResponse = (cooldown: RecievedCooldown) => any; + +function add( + items: Array< + | [CooldownLocation | keyof typeof CooldownLocation, CooldownString] + | Cooldown + >, + message?: CooldownResponse +): EventPlugin { + const raw = items.map((c) => { + if (!Array.isArray(c)) return c; + return parseCooldown(c[0] as CooldownLocation, c[1]); + }) as Array; + + 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(); + }, + }; +} + +type Location = (value: CooldownString) => ReturnType; + +const locations: Record = { + [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/src/plugins/refreshCache.ts b/src/plugins/refreshCache.ts new file mode 100644 index 0000000..a710cea --- /dev/null +++ b/src/plugins/refreshCache.ts @@ -0,0 +1,22 @@ +import { CommandPlugin, CommandType, PluginType } from "@sern/handler"; +import type { Collection } from "discord.js"; +import { cp } from "../commands/eval"; +import type { Data } from "../commands/plugin"; +export function refreshCache(): CommandPlugin { + return { + type: PluginType.Command, + description: "refreshes cache", + async execute(wrapper, payload, controller) { + const cache = await cp(wrapper.client); + wrapper.client.cache = cache; + console.log("Cached plugins for the first time"); + return controller.next(); + }, + }; +} + +declare module "discord.js" { + interface Client { + cache: Collection | null; + } +} From 3e12960a39e528b87c985eda03a338aaeb4f1b1b Mon Sep 17 00:00:00 2001 From: EvolutionX Date: Sat, 6 Aug 2022 13:03:24 +0530 Subject: [PATCH 2/3] chore: oops --- src/index.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index 375be58..dec3679 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,22 +30,3 @@ client.once("ready", (client) => { }); client.login(); -// const d = `// @ts-nocheck -// /** -// * This is dmOnly plugin, it allows commands to be run only in DMs. -// * -// * @author @EvolutionX-10 -// * @version 1.1.0-beta -// * @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 -// * }) -// * \`\`\` -// */` - -// console.log(parse(d, ['@license'])); \ No newline at end of file From 0bdd4100bd7a85d4b197d63674df296f350dcb68 Mon Sep 17 00:00:00 2001 From: EvolutionX Date: Sat, 6 Aug 2022 13:09:27 +0530 Subject: [PATCH 3/3] chore: done --- src/commands/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/plugin.ts b/src/commands/plugin.ts index f48114c..24cbd7c 100644 --- a/src/commands/plugin.ts +++ b/src/commands/plugin.ts @@ -50,7 +50,7 @@ export default commandModule({ async execute(ctx, [, options]) { const url = options.getString("plugin", true) as string; const name = ctx.client.cache?.findKey((d) => d.download_url === url); - let data = await fetch(url, { method: "GET" }) + let data = await fetch(url) .then((r) => r.text()) .catch(() => null); if (!data || !name)