commit 9520847cc9bb0ad50383942357ecebe51cde983b Author: ZomoXYZ Date: Tue Mar 29 22:31:38 2022 -0500 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..888997e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +lib +examples/*.js \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d5ee5c5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ + + +Copyright 2022 Ashley Zomo + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..23266f4 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# discordslash + +Discord Application Commands made easy + +## usage + +run `npm run buildexample ` to compile example(s) + +`examples/main.ts` + +```ts +import { Client, CommandInteraction, Intents } from 'discord.js'; +import { addCommand, CommandGenerator, CommandOptionGenerator, initClient } from '../'; + +const client = new Client({ intents: [Intents.FLAGS.GUILDS] }); + +initClient(client); + +addCommand([ + new CommandGenerator() + .setName('ping') + .setDescription('ping me') + .setRun((interaction: CommandInteraction) => + interaction.reply('pong')), + + new CommandGenerator() + .setName('8ball') + .setDescription('ask the magic 8ball a question') + .addOption([ + + new CommandOptionGenerator() + .setName('question') + .setType('string') + .setDescription('question to ask the 8ball') + .setRequired() + + ]) + .setRun(EightBall) +]); + +client.login(TOKEN); +``` + +## TODO + +- quick option functions +- non global commmands diff --git a/buildexample.js b/buildexample.js new file mode 100644 index 0000000..ecbfb5c --- /dev/null +++ b/buildexample.js @@ -0,0 +1,16 @@ +const { build } = require('esbuild'); + +const define = { + TOKEN: `"${process.argv[2]}"` +}; + +build({ + entryPoints: ['examples/main.ts'], + outfile: 'examples/main.js', + + target: 'es6', + platform: 'node', + format: 'cjs', + + define +}); \ No newline at end of file diff --git a/examples/index.d.ts b/examples/index.d.ts new file mode 100644 index 0000000..a3c2515 --- /dev/null +++ b/examples/index.d.ts @@ -0,0 +1 @@ +declare const TOKEN: string; \ No newline at end of file diff --git a/examples/main.ts b/examples/main.ts new file mode 100644 index 0000000..44fcaf3 --- /dev/null +++ b/examples/main.ts @@ -0,0 +1,72 @@ +import { Client, CommandInteraction, Intents } from 'discord.js'; +import { addCommand, CommandGenerator, CommandOptionGenerator, initClient } from '../lib/main'; + +const EightBallAnswers = [ + 'It is certain', + 'It is decidedly so', + 'Without a doubt', + 'Yes - definitely', + 'You may rely on it', + 'As I see it, yes', + 'Most likely', + 'Outlook good', + 'Yes', + 'Signs point to yes', + 'Don\'t count on it', + 'My reply is no', + 'My sources say no', + 'Outlook not so good', + 'Very doubtful', + 'Reply hazy, try again', + 'Ask again later', + 'Better not tell you now', + 'Cannot predict now', + 'Concentrate and ask again' +]; + +const client = new Client({ intents: [Intents.FLAGS.GUILDS] }); + +initClient(client); + +addCommand([ + new CommandGenerator() + .setName('ping') + .setDescription('ping me') + .setRun((interaction: CommandInteraction) => + interaction.reply('pong')), + + new CommandGenerator() + .setName('8ball') + .setDescription('ask the magic 8ball a question') + //.addOption('question', 'string', 'question to ask the 8ball') //will add something like this later + .addOption([ + + new CommandOptionGenerator() + .setName('question') + .setType('string') + .setDescription('question to ask the 8ball') + .setRequired() + + ]) + .setRun(EightBall) +]); + +function EightBall(interaction: CommandInteraction) { + + let question = interaction.options.get('question', true).value, + answer = EightBallAnswers[Math.floor(Math.random() * EightBallAnswers.length)]; + + if (typeof question === 'string') { + + if (!question.endsWith('?')) + question += '?'; + + interaction.reply(`\`${question}\` ${answer}`); + + } + +} + +client.on('ready', client => console.log(`Ready ${client.user.tag}`)); + +client.login(TOKEN); diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..bc2c4dc --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "strict": true + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c97524 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "discordslash", + "description": "", + "version": "1.0.0", + "main": "lib/main.js", + "types": "lib/main.d.ts", + "devDependencies": {}, + "scripts": { + "build": "tsc", + "buildexamples": "node buildexample.js", + "_prepare": "npm run build" + }, + "repository": { + "type": "git", + "url": "git@git.zomo.dev:zomo/discordslash.git" + }, + "author": "ashley zomo", + "license": "ISC", + "dependencies": { + "discord.js": "^13.6.0", + "typescript": "^4.6.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..44a3bef --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,182 @@ +lockfileVersion: 5.3 + +specifiers: + discord.js: ^13.6.0 + typescript: ^4.6.3 + +dependencies: + discord.js: 13.6.0 + typescript: 4.6.3 + +packages: + + /@discordjs/builders/0.11.0: + resolution: {integrity: sha512-ZTB8yJdJKrKlq44dpWkNUrAtEJEq0gqpb7ASdv4vmq6/mZal5kOv312hQ56I/vxwMre+VIkoHquNUAfnTbiYtg==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + discord-api-types: 0.26.1 + ts-mixer: 6.0.1 + tslib: 2.3.1 + zod: 3.14.3 + dev: false + + /@discordjs/collection/0.4.0: + resolution: {integrity: sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + dev: false + + /@sapphire/async-queue/1.3.0: + resolution: {integrity: sha512-z+CDw5X4UgIEpZL8KM+ThVx1i8V60HBg0l/oFewTNbQQeRDJHdVxHyJykv+SF1H+Rc8EkMS81VTWo95jVYgO/g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@sindresorhus/is/4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: false + + /@types/node-fetch/2.6.1: + resolution: {integrity: sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==} + dependencies: + '@types/node': 17.0.23 + form-data: 3.0.1 + dev: false + + /@types/node/17.0.23: + resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==} + dev: false + + /@types/ws/8.5.3: + resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} + dependencies: + '@types/node': 17.0.23 + dev: false + + /asynckit/0.4.0: + resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} + dev: false + + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /delayed-stream/1.0.0: + resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} + engines: {node: '>=0.4.0'} + dev: false + + /discord-api-types/0.26.1: + resolution: {integrity: sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==} + engines: {node: '>=12'} + dev: false + + /discord.js/13.6.0: + resolution: {integrity: sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g==} + engines: {node: '>=16.6.0', npm: '>=7.0.0'} + dependencies: + '@discordjs/builders': 0.11.0 + '@discordjs/collection': 0.4.0 + '@sapphire/async-queue': 1.3.0 + '@types/node-fetch': 2.6.1 + '@types/ws': 8.5.3 + discord-api-types: 0.26.1 + form-data: 4.0.0 + node-fetch: 2.6.7 + ws: 8.5.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: false + + /form-data/3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /node-fetch/2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /tr46/0.0.3: + resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=} + dev: false + + /ts-mixer/6.0.1: + resolution: {integrity: sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==} + dev: false + + /tslib/2.3.1: + resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + dev: false + + /typescript/4.6.3: + resolution: {integrity: sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + + /webidl-conversions/3.0.1: + resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} + dev: false + + /whatwg-url/5.0.0: + resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /ws/8.5.0: + resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /zod/3.14.3: + resolution: {integrity: sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ==} + dev: false diff --git a/src/discord/main.ts b/src/discord/main.ts new file mode 100644 index 0000000..165778b --- /dev/null +++ b/src/discord/main.ts @@ -0,0 +1,99 @@ +import { MessageEmbed } from 'discord.js'; +import { template, resolveColor, bigString } from '../main'; +import { embedObject, basicObjectString, authorData, footerData } from '../types'; + +export * from "../main"; + +/** + * converts embedObj to Discord.MessageEmbed + */ +export function embedObjEmbed(embedObj: embedObject, args: basicObjectString = {}): MessageEmbed { + const embed = new MessageEmbed(), + { author, color, description, fields, footer, image, thumbnail, timestamp, title, url } = embedObj; + + if (author !== undefined) { + + let authorFix: authorData; + + if (typeof author === 'string') + authorFix = { + name: template(author, args) + }; + else { + + const {name, iconURL, url} = author; + + authorFix = { + name: template(name, args) + }; + + if (iconURL !== undefined) + authorFix.iconURL = template(iconURL, args); + + if (url !== undefined) + authorFix.url = template(url, args); + + } + + embed.setAuthor(authorFix); + + } + + if (footer !== undefined) { + + let footerFix: footerData; + + if (typeof footer === 'string') { + + footerFix = { + text: template(footer, args) + }; + + } else { + + const {text, iconURL} = footer; + + footerFix = { + text: template(text, args) + }; + + if (iconURL !== undefined) + footerFix.iconURL = template(iconURL, args); + + } + + embed.setFooter(footerFix); + + } + + if (color !== undefined) + embed.setColor(resolveColor(template(color, args))); + + if (description !== undefined) + embed.setDescription(template(bigString(description), args)); + + if (image !== undefined) + embed.setImage(template(image, args)); + + if (thumbnail !== undefined) + embed.setThumbnail(template(thumbnail, args)); + + if (title !== undefined) + embed.setTitle(template(title, args)); + + if (url !== undefined) + embed.setURL(template(url, args)); + + if (timestamp === true) + embed.setTimestamp(); + else if (typeof timestamp === 'string') + embed.setTimestamp(new Date(template(timestamp, args))); + else if (timestamp !== false) + embed.setTimestamp(timestamp); + + fields?.forEach(field => { + embed.addField(template(field.name, args), template(bigString(field.value), args), field.inline); + }); + + return embed; +} \ No newline at end of file diff --git a/src/discord/types.ts b/src/discord/types.ts new file mode 100644 index 0000000..bdb3962 --- /dev/null +++ b/src/discord/types.ts @@ -0,0 +1,9 @@ +import { MessageEmbed } from "discord.js"; + +/** + * an object that contains embeds and can be passed directly to methods like `Discord.TextChannel.send()` + */ +export interface embedDataType { + content?: string, + embeds: MessageEmbed[] +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..4a2f1f0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,76 @@ + +import { authorData, basicObject, basicObjectString, bigStringType, embedObject, footerData } from './types'; + +/** + * + * @param str + * @param args + * @returns + */ +export function template(str: string, args: basicObject): string { + + return str.replace(/{\w+}/g, str => { + + const key = str.substring(1, str.length-1); + + if (key in args) + return args[key]; + + return key; + + }); + +} + +/** + * converts bigString to string + */ +export function bigString(bigStr: bigStringType): string { + if (Array.isArray(bigStr)) + return bigStr.join('\n'); + return bigStr; +} + + +/** + * converts Hex Color string to an RGB array + */ +export function resolveColor(color: string): [number, number, number] { + color = color.replace(/[^0-9a-f]/gi, ''); + + const colorNum: [number, number, number] = [0, 0, 0]; + + if (color.length === 3 || color.length === 6) { + + const colorSplRaw = /([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/.exec(color); + + if (!colorSplRaw) + return colorNum; + + const colorSpl = colorSplRaw.slice(1, 4); + + if (color.length === 3) + colorSpl.map(c => c + c); + + for (let i = 0; i < colorSpl.length && i < colorNum.length; i++) + colorNum[i] = parseInt(colorSpl[i], 16); + + } + + return colorNum; +} + +/** + * converts embedObj to a string if applicable + * @param fallback the string to use if no valid strings can be found + */ +export function embedObjStr(embedObj: embedObject, args: basicObjectString = {}, fallback = ''): string { + + if (embedObj.content !== undefined) + return template(bigString(embedObj.content), args); + + if (embedObj.description !== undefined) + return template(bigString(embedObj.description), args); + + return fallback; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a5b8ae1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,99 @@ + + +/** + * any indexable object + */ +//this is a generic type, and needs 'any' +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type basicObject = {[keys: string]: any}; + +/** + * any indexable object with string values + */ +export type basicObjectStringable = {[keys: string]: string|number|boolean|null}; +export type basicObjectString = {[keys: string]: string}; + +/** + * an abstract version of strings + */ +export type bigStringType = string | string[]; + + +/** + * a representation of an author in the LANG object + * + * `LANG > Language > Embed > Field` + */ +export interface embedField { + name: string, + value: bigStringType, + inline?: boolean +} + +/** + * a representation of an author in the LANG object + * + * `LANG > Language > Embed > Author` + */ +export interface authorData { + name: string, + url?: string, + iconURL?: string +} + +/** + * a representation of a footer in the LANG object + * + * `LANG > Language > Embed > Footer` + */ +export interface footerData { + text: string, + iconURL?: string +} + +/** + * a representation of an embed in the LANG object + * + * `LANG > Language > Embed` + */ +export interface embedObject { + embed: true, + + content?: string, + title?: string, + description?: bigStringType, + /** + * URL + */ + url?: string, + /** + * #FFFFFF + */ + color?: string, + footer?: string | footerData, + thumbnail?: string, + /** + * URL + */ + image?: string, + /** + * URL + */ + author?: string | authorData, + fields?: embedField[], + timestamp?: boolean | string | number +} + +/** + * a specific language in the LANG object + * + * `LANG > Language` + */ +export type LangObj = { [keys:string]: LangObj | embedObject | string } + +/** + * the entire LANG object + * + * `LANG` + */ +export type LangObjWhole = { [langid:string]: LangObj } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c698472 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "declaration": true, + "outDir": "./lib", + "strict": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} \ No newline at end of file