NeonJS
/
framework
Archived
0
0
Fork 0

feat: initial project

This commit is contained in:
Daryl Ronningen 2021-11-01 21:47:23 -07:00
commit 683357fec1
Signed by: Daryl Ronningen
GPG Key ID: FD23F0C934A5EC6B
25 changed files with 10325 additions and 0 deletions

73
.drone.yml Normal file
View File

@ -0,0 +1,73 @@
kind: pipeline
type: docker
name: default
steps:
- name: install
image: node:16
commands:
- echo ===== INSTALLING DEPENDENCIES =====
- yarn install
- name: build
image: node:16
commands:
- echo ===== BUILDING APPLICATION =====
- yarn build
- name: publish-dev
image: node:16
commands:
- echo ===== PUBLISHING APPLICATION =====
- echo "//registry.npmjs.org/:_authToken=$${NPM_TOKEN}" > ~/.npmrc
- apt update
- apt install -y jq
- npm version --git-tag-version=false $$(jq --raw-output '.version' package.json)-$$(git rev-parse HEAD).$$(date +%s)
- npm publish --tag dev --access public
environment:
NPM_TOKEN:
from_secret: NPM_TOKEN
when:
branch:
- master
event:
- push
- name: publish-stable
image: node:16
commands:
- echo ===== PUBLISHING APPLICATION =====
- yarn semantic-release
environment:
GITEA_URL: "https://code.relms.dev"
GITEA_TOKEN:
from_secret: GITEA_TOKEN
NPM_TOKEN:
from_secret: NPM_TOKEN
GIT_AUTHOR_NAME: "DroneCI"
GIT_AUTHOR_EMAIL: "drone@relms.dev"
GIT_COMMITTER_NAME: "DroneCI"
GIT_COMMITTER_EMAIL: "drone@relms.dev"
when:
branch:
- stable
event:
- push
# - name: discord-notification
# image: appleboy/drone-discord
# settings:
# webhook_id:
# from_secret: DISCORD_WEBHOOK_ID
# webhook_token:
# from_secret: DISCORD_WEBHOOK_TOKEN
# message: >
# {{#success build.status}}
# Build {{build.number}} succeeded.
# {{else}}
# Build {{build.number}} failed.
# {{/success}}
# when:
# status:
# - success
# - failure

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = tab
indent_size = 2
tab_width = 2
trim_trailing_whitespace = true

126
.eslintrc Normal file
View File

@ -0,0 +1,126 @@
{
"root": true,
"env": {
"es2021": true,
"node": true,
"commonjs": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"unicorn"
],
"rules": {
// Base Eslint Rules
"indent": [
"error",
"tab",
{
"SwitchCase": 1,
"CallExpression": {
"arguments": 1
}
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"eol-last": [
"error",
"always"
],
"object-curly-spacing": [
"error",
"always"
],
"default-case": "error",
"comma-dangle": [
"error",
"always-multiline"
],
"no-case-declarations": "off",
"camelcase": "off",
"keyword-spacing": "error",
"spaced-comment": [
"error",
"always"
],
"no-var": "error",
"eqeqeq": "error",
"no-eq-null": "error",
"arrow-parens": [
"error",
"always"
],
// TypeScript Rules
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "class",
"format": [
"PascalCase"
]
},
{
"selector": "classProperty",
"modifiers": [
"private"
],
"format": [
"camelCase"
],
"leadingUnderscore": "require"
},
{
"selector": "interface",
"format": null,
"custom": {
"regex": "^I",
"match": true
}
},
{
"selector": "enum",
"format": null,
"custom": {
"regex": "^E",
"match": true
}
}
],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/explicit-function-return-type": "error",
// Unicorn Rules
"unicorn/filename-case": [
"error",
{
"case": "camelCase"
}
]
},
"reportUnusedDisableDirectives": true
}

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/.yarn/*
!/.yarn/patches
!/.yarn/plugins
!/.yarn/releases
!/.yarn/sdks
node_modules
.DS_store
dist

4
.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16.11.0

54
.releaserc Normal file
View File

@ -0,0 +1,54 @@
{
"branches": [
"stable"
],
"repositoryUrl": "https://code.relms.dev/NeonJS/framework",
"ci": true,
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"parserOpts": {
"noteKeywords": [
"BREAKING CHANGE",
"BREAKING CHANGES"
]
}
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"parserOpts": {
"noteKeywords": [
"BREAKING CHANGE",
"BREAKING CHANGES"
]
}
}
],
"@semantic-release/changelog",
[
"@semantic-release/npm",
{
"tarballDir": "dist"
}
],
[
"@saithodev/semantic-release-gitea",
{
"assets": [
"dist/*.tgz"
]
}
],
[
"@semantic-release/git",
{
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
]
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

631
.yarn/releases/yarn-3.0.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

13
.yarnrc.yml Normal file
View File

@ -0,0 +1,13 @@
nmMode: hardlinks-local
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
yarnPath: .yarn/releases/yarn-3.0.2.cjs

6
CHANGELOG.md Normal file
View File

@ -0,0 +1,6 @@
### [1.0.1](https://code.relms.dev/NeonJS/framework/compare/v1.0.0...v1.0.1) (2021-11-02)
### Bug Fixes
* pino pretty deprecation ([90c6de1](https://code.relms.dev/NeonJS/framework/commit/90c6de1b5d44e196f5666c8cee56b1cde6ba46af))

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright © 2021-2021 Daryl Ronningen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1
README.md Normal file
View File

@ -0,0 +1 @@
# framework

98
package.json Normal file
View File

@ -0,0 +1,98 @@
{
"name": "@neonjs/framework",
"version": "1.0.1",
"description": "Discord.JS Slash Commands based framework",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": "https://code.relms.dev/NeonJS/framework",
"bugs": "https://code.relms.dev/NeonJS/framework/issues",
"homepage": "https://code.relms.dev/NeonJS/framework/src/branch/master/README.md",
"keywords": [
"Discord",
"framework",
"discord.js",
"neon",
"neonjs"
],
"author": "Daryl Ronningen <relms@relms.dev>",
"maintainers": [],
"contributors": [],
"engines": {
"node": ">=16.*"
},
"files": [
"dist",
"package.json",
"LICENSE",
"README.md",
"CHANGELOG"
],
"scripts": {
"build": "tsc",
"clean": "rimraf dist",
"commit": "cz",
"lint": "eslint --format=pretty src",
"lint:fix": "eslint --format=pretty src --fix",
"postinstall": "husky install",
"prepublishOnly": "pinst --disable",
"postpublish": "pinst --enable",
"release": "semantic-release"
},
"dependencies": {
"@discordjs/rest": "^0.1.0-canary.0",
"discord-api-types": "^0.24.0",
"discord.js": "^13.3.1",
"lodash": "^4.17.21",
"node-fetch": "^3.0.0",
"pino": "^7.0.5",
"pino-pretty": "^7.1.0"
},
"devDependencies": {
"@commitlint/cli": "^14.1.0",
"@commitlint/config-conventional": "^14.1.0",
"@commitlint/cz-commitlint": "^14.1.0",
"@saithodev/semantic-release-gitea": "^2.1.0",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/commit-analyzer": "^9.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "^8.0.2",
"@semantic-release/release-notes-generator": "^10.0.2",
"@types/eslint": "^7.28.2",
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.6",
"@types/pino": "^6.3.12",
"@types/semantic-release": "^17.2.2",
"@types/source-map-support": "^0.5.4",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"@typescript-eslint/typescript-estree": "^5.3.0",
"commitizen": "^4.2.4",
"eslint": "^8.1.0",
"eslint-formatter-pretty": "^4.1.0",
"eslint-plugin-unicorn": "^37.0.1",
"husky": "^7.0.4",
"inquirer": "^8.2.0",
"lint-staged": "^11.2.6",
"pinst": "^2.1.6",
"rimraf": "^3.0.2",
"semantic-release": "^18.0.0",
"source-map-support": "^0.5.20",
"typescript": "^4.4.4"
},
"lint-staged": {
"src/**/*": [
"yarn lint"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"config": {
"commitizen": {
"path": "@commitlint/cz-commitlint"
}
}
}

5
src/index.ts Normal file
View File

@ -0,0 +1,5 @@
export { Command } from './lib/structures/command';
export * from './lib/utils/augments';
export * from './lib/utils/types';
export * from './lib/utils/utils';
export * from './lib/neonClient';

201
src/lib/neonClient.ts Normal file
View File

@ -0,0 +1,201 @@
import { REST } from '@discordjs/rest';
import { APIApplicationCommand, Routes } from 'discord-api-types/v9';
import { ApplicationCommand, ApplicationCommandOptionData, ChatInputApplicationCommandData, Client, ClientOptions, Collection } from 'discord.js';
import _ from 'lodash';
import path from 'path';
import pino from 'pino';
import PinoPretty from 'pino-pretty';
import { walkDir } from './utils/utils';
import type { Command } from './structures/command';
const commandCooldowns: Collection<string, Collection<string, number>> = new Collection();
const slashCommands: Collection<string, ApplicationCommand> = new Collection();
let isBotReady = false;
export class NeonClient extends Client {
public commands: Collection<string, Command> = new Collection();
public ownerID: string;
public static logger: pino.Logger = pino({ level: 'trace', prettyPrint: { levelFirst: true, colorize: true }, prettifier: PinoPretty });
public constructor(ownerID: string, options: ClientOptions) {
super(options);
this.ownerID = ownerID;
this.once('ready', async () => {
const getCommands = await new REST({ version: '9' }).setToken(this.token as string).get(Routes.applicationCommands(this.application!.id)) as APIApplicationCommand[];
getCommands.forEach((val) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const command: ApplicationCommand = new ApplicationCommand(this, val);
slashCommands.set(command.id, command);
});
this.logger.info('Loading Commands...');
try {
const files = await walkDir(`${path.dirname(require.main!.filename)}/commands`);
files.forEach(async (file) => {
if (file.endsWith('js')) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fileCommand = require(file);
let command: Command;
fileCommand['default'] !== undefined ? command = new fileCommand['default'](this, file) : command = new fileCommand(this, file);
this.commands.set(command.options.name!, command);
if (!slashCommands.find((int) => int.name === command.options.name)) {
this.logger.debug(`Creating new command ${command.options.name}`);
const commandOptions: ApplicationCommandOptionData[] = [];
command.options.args?.forEach((arg) => {
commandOptions.push(arg);
});
this.application?.commands.create({
name: command.options.name!,
description: command.options.shortDescription!,
options: commandOptions,
});
} else {
const commandOptions: ApplicationCommandOptionData[] = [];
command.options.args?.forEach((arg) => {
commandOptions.push(arg);
});
const fileCommand: ChatInputApplicationCommandData = {
name: command.options.name!,
description: command.options.shortDescription!,
options: commandOptions,
};
const cacheCommand1 = slashCommands.find((cmd) => cmd.name === fileCommand.name)?.toJSON() as ChatInputApplicationCommandData;
const cacheCommand2 = { name: cacheCommand1.name, description: cacheCommand1.description, options: cacheCommand1.options };
if (!_.isEqual(fileCommand, cacheCommand2)) {
this.logger.debug(`Editing command ${fileCommand.name}`);
this.application?.commands.edit(slashCommands.find((int) => int.name === command.options.name)!, fileCommand);
}
}
this.logger.debug(`Loaded command ${command.options.name}`);
}
});
slashCommands.forEach((command) => {
if (!this.commands.find((cmd) => command.name === cmd.options.name)) {
this.application?.commands.delete(command.id);
this.logger.debug(`Deleting command ${command.name}`);
}
});
this.logger.info(`Finished loading commands! Found ${this.commands.size} commands.`);
} catch (err) {
this.logger.error(`An error has occurred while attempting to load command files! Please see error below\n${(err as Error).message}`);
}
isBotReady = true;
});
this.on('interactionCreate', async (interaction) => {
if (!isBotReady) return;
if (!interaction.isCommand()) return;
const findCommand = this.commands.find((com) => com.options.name === interaction.commandName);
if (!findCommand) return;
if (findCommand.options.ownerOnly && interaction.user.id !== this.ownerID) {
await interaction.reply('This command can be ran by the bot owner only!');
this.logger.warn(`$${interaction.user.username} tried running command ${findCommand.options.name} but doesn't have the permissions to.`);
return;
}
if ((findCommand.options.runIn === 'dms') && interaction.channel?.type !== 'DM') {
await interaction.reply('This command can only be ran in DMs!');
this.logger.warn(`$${interaction.user.username} tried running command ${findCommand.options.name} in a server but the command can only be ran in a DM.`);
return;
}
if ((findCommand.options.runIn === 'servers') && interaction.channel?.type !== 'GUILD_TEXT') {
await interaction.reply('This command can only be ran in a Server!');
this.logger.warn(`$${interaction.user.username} tried running command ${findCommand.options.name} in a DM but the command can only be ran in a Server.`);
return;
}
if (interaction.guild) for (let index = 0; index < findCommand.options.requiredBotPermissions!.length; index++) {
const permission = findCommand.options.requiredBotPermissions![index]!;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!interaction.guild.me!.roles.highest.permissions.toArray(true).includes(permission.toString())) {
await interaction.reply(`The bot is missing the permission \`${permission}\`! If you believe this is a mistake, please bring it up with a server admin.`);
return;
}
}
if (interaction.member) if (typeof interaction.member.permissions === 'string') {
// TODO!
} else {
for (let index = 0; index < findCommand.options.requiredBotPermissions!.length; index++) {
const permission = findCommand.options.requiredBotPermissions![index]!;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!interaction.member.permissions.toArray(true)?.includes(permission.toString())) {
await interaction.reply(`You are missing the permission \`${permission}\`!`);
return;
}
}
}
if (interaction.user.id !== this.ownerID) {
if (!commandCooldowns.has(findCommand.options.name!)) {
commandCooldowns.set(findCommand.options.name!, new Collection());
}
const now = Date.now();
const timestamps = commandCooldowns.get(findCommand.options.name!);
const cooldownAmount = findCommand.options.cooldown! * 1000;
if (timestamps!.has(interaction.user.id)) {
const expirationTime = timestamps!.get(interaction.user.id)! + cooldownAmount;
if (now < expirationTime) {
const timeLeft = (expirationTime - now) / 1000;
await interaction.reply(`Please wait ${timeLeft} before running ${findCommand.options.name} again!`);
return;
}
}
timestamps!.set(interaction.user.id, now);
setTimeout(() => timestamps!.delete(interaction.user.id), cooldownAmount);
}
try {
this.logger.info(`Command ${findCommand.options.name} is being ran in ${interaction.guild ? interaction.guild.name : 'DMs'} by ${interaction.user.username}`);
await findCommand.run(interaction);
this.logger.info(`Finished running ${findCommand.options.name} in ${interaction.guild ? interaction.guild.name : `${interaction.user.username} DMs`}`);
} catch (e) {
this.logger.error(`An error has occurred while running ${findCommand.options.name}!\n${(e as Error).message}`);
await interaction.reply(`An error has ocurred while running this command. Please pass this error on to the bot developers\n${(e as Error).message}`);
}
});
}
}

View File

@ -0,0 +1,29 @@
import _ from 'lodash';
import path from 'path';
import type { Client, CommandInteraction } from 'discord.js';
import type { ICommandOptions } from '../utils/types';
export abstract class Command {
public readonly client: Client;
public readonly file: string;
public readonly options: ICommandOptions;
protected constructor(client: Client, file: string, options: ICommandOptions) {
this.client = client;
this.file = file;
const defaultOptions: ICommandOptions = {
cooldown: 5,
group: path.basename(path.dirname(this.file)) === 'commands' ? '' : path.basename(path.dirname(this.file)),
name: path.basename(this.file, path.extname(this.file)),
shortDescription: '',
requiredBotPermissions: [],
requiredUserPermissions: [],
};
this.options = _.merge(defaultOptions, options);
}
public abstract run(interaction: CommandInteraction): Promise<void> | void;
}

11
src/lib/utils/augments.ts Normal file
View File

@ -0,0 +1,11 @@
import type { Collection } from 'discord.js';
import type pino from 'pino';
import type { Command } from '../structures/command';
declare module 'discord.js' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Client {
commands: Collection<string, Command>;
logger: pino.Logger;
}
}

15
src/lib/utils/types.ts Normal file
View File

@ -0,0 +1,15 @@
import type { ApplicationCommandOptionData, PermissionResolvable } from 'discord.js';
export interface ICommandOptions {
args?: ApplicationCommandOptionData[];
cooldown?: number;
extendedDescription?: string;
group?: string;
name?: string;
ownerOnly?: boolean;
requiredBotPermissions?: PermissionResolvable[]
requiredUserPermissions?: PermissionResolvable[]
runIn?: 'both' | 'dms' | 'servers';
shortDescription: string;
usage?: string;
}

63
src/lib/utils/utils.ts Normal file
View File

@ -0,0 +1,63 @@
import fs from 'fs';
import path from 'path';
export const walkDir = async (dir: string): Promise<string[]> => {
let results: string[] = [];
return new Promise((resolve, reject) => {
fs.readdir(dir, (err, list) => {
if (err) return reject(err);
let pending = list.length;
if (!pending) return resolve(results);
list.forEach((file) => {
file = path.resolve(dir, file);
fs.stat(file, async (err, stat) => {
if (err) return reject(err);
if (stat && stat.isDirectory()) {
try {
const dir = await walkDir(file);
results = results.concat(dir as unknown as string);
if (!--pending) resolve(results);
} catch (err) {
reject(err);
}
} else {
results.push(file);
if (!--pending) resolve(results);
}
});
});
});
});
};
// eslint-disable-next-line @typescript-eslint/ban-types
export type Builtin = Date | Error | Function | Primitives | RegExp;
export type DeepRequired<T> = T extends Builtin
? NonNullable<T>
: T extends Map<infer K, infer V>
? Map<DeepRequired<K>, DeepRequired<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepRequired<K>, DeepRequired<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepRequired<K>, DeepRequired<V>>
: T extends Set<infer U>
? Set<DeepRequired<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepRequired<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepRequired<U>>
: T extends Promise<infer U>
? Promise<DeepRequired<U>>
// eslint-disable-next-line @typescript-eslint/ban-types
: T extends {}
? { [K in keyof T]-?: DeepRequired<T[K]> }
: NonNullable<T>;
export type Primitives = bigint | boolean | null | number | string | symbol | undefined;

48
tsconfig.json Normal file
View File

@ -0,0 +1,48 @@
{
"compilerOptions": {
"declaration": true,
"lib": [
"ES2021",
"DOM"
],
"module": "CommonJS",
"outDir": "dist",
"removeComments": false,
"target": "ES2021",
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"disableSizeLimit": true,
"explainFiles": false,
"extendedDiagnostics": false,
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error",
"listEmittedFiles": false,
"listFiles": false,
"newLine": "lf",
"noEmitOnError": false,
"preserveConstEnums": true,
"traceResolution": false,
"moduleResolution": "Node"
},
"include": [
"src/**/*.ts"
]
}

8178
yarn.lock Normal file

File diff suppressed because it is too large Load Diff