From 03ea5df55189ca5783037b671e27e3412f74cd67 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Tue, 22 Dec 2020 12:28:45 +0530 Subject: [PATCH] feat: middlewares and http-based slash util --- README.md | 19 +++-- src/models/client.ts | 2 +- src/models/slashClient.ts | 155 +++++++++++++++++++++++++++++++++++--- 3 files changed, 154 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 767b21b..585d405 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,12 @@
- Lightweight and easy to use. -- Built-in Command Framework, - - Easily build Commands on the fly. - - Completely Customizable. - - Complete Object-Oriented approach. -- 100% Discord API Coverage. -- Customizable caching. - - Built in support for Redis. - - Write Custom Cache Adapters. -- Complete TypeScript support. +- Complete Object-Oriented approach. +- Slash Commands supported. +- Built-in Commands framework. +- Customizable Caching, with Redis support. +- Use `@decorators` to easily make things! +- Made with ❤️ TypeScript. ## Table of Contents @@ -102,13 +99,14 @@ client.connect('super secret token comes here', Intents.All) ``` Or with Decorators! + ```ts import { Client, event, Intents, command, - CommandContext, + CommandContext } from 'https://deno.land/x/harmony/mod.ts' class MyClient extends CommandClient { @@ -141,6 +139,7 @@ Documentation is available for `main` (branch) and `stable` (release). - [Main](https://doc.deno.land/https/raw.githubusercontent.com/harmony-org/harmony/main/mod.ts) - [Stable](https://doc.deno.land/https/deno.land/x/harmony/mod.ts) +- [Guide](https://harmony-org.github.io) ## Found a bug or want support? Join our discord server! diff --git a/src/models/client.ts b/src/models/client.ts index 547009a..2fc4f03 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -288,7 +288,7 @@ export function groupslash( name?: string, guild?: string ) { - return function (client: Client | SlashModule, prop: string) { + return function (client: Client | SlashModule | SlashClient, prop: string) { if (client._decoratedSlash === undefined) client._decoratedSlash = [] const item = (client as { [name: string]: any })[prop] if (typeof item !== 'function') { diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 29effff..c652d58 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -11,6 +11,14 @@ import { import { Collection } from '../utils/collection.ts' import { Client } from './client.ts' import { RESTManager } from './rest.ts' +import { SlashModule } from './slashModule.ts' +import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' +import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' +import { + Request as ORequest, + Response as OResponse +} from 'https://deno.land/x/opine@1.0.0/src/types.ts' +import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts' export class SlashCommand { slash: SlashCommandsManager @@ -37,6 +45,21 @@ export class SlashCommand { async edit(data: SlashCommandPartial): Promise { await this.slash.edit(this.id, data, this._guild) } + + /** Create a handler for this Slash Command */ + handle( + func: SlashCommandHandlerCallback, + options?: { parent?: string; group?: string } + ): SlashCommand { + this.slash.slash.handle({ + name: this.name, + parent: options?.parent, + group: options?.group, + guild: this._guild, + handler: func + }) + return this + } } export interface CreateOptions { @@ -58,7 +81,7 @@ function createSlashOption( ? undefined : data.description ?? 'No description.', options: data.options?.map((e) => - typeof e === 'function' ? e(SlashOptionCallableBuilder) : e + typeof e === 'function' ? e(SlashOption) : e ), choices: data.choices === undefined @@ -70,7 +93,7 @@ function createSlashOption( } // eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class SlashOptionCallableBuilder { +export class SlashOption { static string(data: CreateOptions): SlashCommandOption { return createSlashOption(SlashCommandOptionType.STRING, data) } @@ -104,9 +127,7 @@ export class SlashOptionCallableBuilder { } } -export type SlashOptionCallable = ( - o: typeof SlashOptionCallableBuilder -) => SlashCommandOption +export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption export type SlashBuilderOptionsData = | Array @@ -125,12 +146,10 @@ function buildOptionsArray( options: SlashBuilderOptionsData ): SlashCommandOption[] { return Array.isArray(options) - ? options.map((op) => - typeof op === 'function' ? op(SlashOptionCallableBuilder) : op - ) + ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op)) : Object.entries(options).map((entry) => typeof entry[1] === 'function' - ? entry[1](SlashOptionCallableBuilder) + ? entry[1](SlashOption) : Object.assign(entry[1], { name: entry[0] }) ) } @@ -163,7 +182,7 @@ export class SlashBuilder { option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { if (this.data.options === undefined) this.data.options = [] this.data.options.push( - typeof option === 'function' ? option(SlashOptionCallableBuilder) : option + typeof option === 'function' ? option(SlashOption) : option ) return this } @@ -312,6 +331,7 @@ export interface SlashOptions { enabled?: boolean token?: string rest?: RESTManager + publicKey?: string } export class SlashClient { @@ -322,6 +342,18 @@ export class SlashClient { commands: SlashCommandsManager handlers: SlashCommandHandler[] = [] rest: RESTManager + modules: SlashModule[] = [] + publicKey?: string + + _decoratedSlash?: Array<{ + name: string + guild?: string + parent?: string + group?: string + handler: (interaction: Interaction) => any + }> + + _decoratedSlashModules?: SlashModule[] constructor(options: SlashOptions) { let id = options.id @@ -332,6 +364,7 @@ export class SlashClient { this.client = options.client this.token = options.token this.commands = new SlashCommandsManager(this) + this.publicKey = options.publicKey if (options !== undefined) { this.enabled = options.enabled ?? true @@ -343,6 +376,24 @@ export class SlashClient { }) } + if (this.client?._decoratedSlashModules !== undefined) { + this.client._decoratedSlashModules.forEach((e) => { + this.modules.push(e) + }) + } + + if (this._decoratedSlash !== undefined) { + this._decoratedSlash.forEach((e) => { + this.handlers.push(e) + }) + } + + if (this._decoratedSlashModules !== undefined) { + this._decoratedSlashModules.forEach((e) => { + this.modules.push(e) + }) + } + this.rest = options.client === undefined ? options.rest === undefined @@ -367,8 +418,16 @@ export class SlashClient { return this } + getHandlers(): SlashCommandHandler[] { + let res = this.handlers + for (const mod of this.modules) { + res = [...res, ...mod.commands] + } + return res + } + private _getCommand(i: Interaction): SlashCommandHandler | undefined { - return this.handlers.find((e) => { + return this.getHandlers().find((e) => { const hasGroupOrParent = e.group !== undefined || e.parent !== undefined const groupMatched = e.group !== undefined && e.parent !== undefined @@ -401,4 +460,78 @@ export class SlashClient { cmd.handler(interaction) } + + async verifyKey( + rawBody: string | Uint8Array | Buffer, + signature: string, + timestamp: string + ): Promise { + if (this.publicKey === undefined) + throw new Error('Public Key is not present') + return edverify( + signature, + Buffer.concat([ + Buffer.from(timestamp, 'utf-8'), + Buffer.from( + rawBody instanceof Uint8Array + ? new TextDecoder().decode(rawBody) + : rawBody + ) + ]), + this.publicKey + ).catch(() => false) + } + + async verifyOpineRequest(req: ORequest): Promise { + const signature = req.headers.get('x-signature-ed25519') + const timestamp = req.headers.get('x-signature-timestamp') + const contentLength = req.headers.get('content-length') + + if (signature === null || timestamp === null || contentLength === null) + return false + + const body = new Uint8Array(parseInt(contentLength)) + await req.body.read(body) + + const verified = await this.verifyKey(body, signature, timestamp) + if (!verified) return false + + return true + } + + /** Middleware to verify request in Opine framework. */ + async verifyOpineMiddleware( + req: ORequest, + res: OResponse, + next: CallableFunction + ): Promise { + const verified = await this.verifyOpineRequest(req) + if (!verified) return res.setStatus(401).end() + + await next() + return true + } + + // TODO: create verifyOakMiddleware too + /** Method to verify Request from Oak server "Context". */ + async verifyOakRequest(ctx: Context): Promise { + const signature = ctx.request.headers.get('x-signature-ed25519') + const timestamp = ctx.request.headers.get('x-signature-timestamp') + const contentLength = ctx.request.headers.get('content-length') + + if ( + signature === null || + timestamp === null || + contentLength === null || + ctx.request.hasBody !== true + ) { + return false + } + + const body = await ctx.request.body().value + + const verified = await this.verifyKey(body as any, signature, timestamp) + if (!verified) return false + return true + } }