Merge pull request #6 from sern-handler/feat/plugin

feat: plugin command
This commit is contained in:
Jacob Nguyen
2022-08-06 02:41:44 -05:00
committed by GitHub
6 changed files with 325 additions and 14 deletions

35
package-lock.json generated
View File

@@ -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",

View File

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

View File

@@ -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<string, Data> = 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;
}

112
src/commands/plugin.ts Normal file
View File

@@ -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)
.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<keyof ParsedData, DocData>;
type B = Record<keyof ParsedData, DocData[]>;

144
src/plugins/cooldown.ts Normal file
View File

@@ -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<K, V> extends Map<K, V> {
public readonly expiry: number;
constructor(
expiry: number = Infinity,
iterable: [K, V][] | ReadonlyMap<K, V> = []
) {
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<string, number>();
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<CommandType.Both> {
const raw = items.map((c) => {
if (!Array.isArray(c)) return c;
return parseCooldown(c[0] as CooldownLocation, c[1]);
}) as Array<Cooldown>;
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<typeof add>;
const locations: Record<CooldownLocation, Location> = {
[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,
};

View File

@@ -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<CommandType.Slash> {
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<string, Data> | null;
}
}