feat: middlewares and http-based slash util

This commit is contained in:
DjDeveloperr 2020-12-22 12:28:45 +05:30
parent da0bfc12c7
commit 03ea5df551
3 changed files with 154 additions and 22 deletions

View file

@ -10,15 +10,12 @@
<br> <br>
- Lightweight and easy to use. - Lightweight and easy to use.
- Built-in Command Framework, - Complete Object-Oriented approach.
- Easily build Commands on the fly. - Slash Commands supported.
- Completely Customizable. - Built-in Commands framework.
- Complete Object-Oriented approach. - Customizable Caching, with Redis support.
- 100% Discord API Coverage. - Use `@decorators` to easily make things!
- Customizable caching. - Made with ❤️ TypeScript.
- Built in support for Redis.
- Write Custom Cache Adapters.
- Complete TypeScript support.
## Table of Contents ## Table of Contents
@ -102,13 +99,14 @@ client.connect('super secret token comes here', Intents.All)
``` ```
Or with Decorators! Or with Decorators!
```ts ```ts
import { import {
Client, Client,
event, event,
Intents, Intents,
command, command,
CommandContext, CommandContext
} from 'https://deno.land/x/harmony/mod.ts' } from 'https://deno.land/x/harmony/mod.ts'
class MyClient extends CommandClient { 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) - [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) - [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! ## Found a bug or want support? Join our discord server!

View file

@ -288,7 +288,7 @@ export function groupslash(
name?: string, name?: string,
guild?: string guild?: string
) { ) {
return function (client: Client | SlashModule, prop: string) { return function (client: Client | SlashModule | SlashClient, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = [] if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop] const item = (client as { [name: string]: any })[prop]
if (typeof item !== 'function') { if (typeof item !== 'function') {

View file

@ -11,6 +11,14 @@ import {
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts' import { Client } from './client.ts'
import { RESTManager } from './rest.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 { export class SlashCommand {
slash: SlashCommandsManager slash: SlashCommandsManager
@ -37,6 +45,21 @@ export class SlashCommand {
async edit(data: SlashCommandPartial): Promise<void> { async edit(data: SlashCommandPartial): Promise<void> {
await this.slash.edit(this.id, data, this._guild) 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 { export interface CreateOptions {
@ -58,7 +81,7 @@ function createSlashOption(
? undefined ? undefined
: data.description ?? 'No description.', : data.description ?? 'No description.',
options: data.options?.map((e) => options: data.options?.map((e) =>
typeof e === 'function' ? e(SlashOptionCallableBuilder) : e typeof e === 'function' ? e(SlashOption) : e
), ),
choices: choices:
data.choices === undefined data.choices === undefined
@ -70,7 +93,7 @@ function createSlashOption(
} }
// eslint-disable-next-line @typescript-eslint/no-extraneous-class // eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class SlashOptionCallableBuilder { export class SlashOption {
static string(data: CreateOptions): SlashCommandOption { static string(data: CreateOptions): SlashCommandOption {
return createSlashOption(SlashCommandOptionType.STRING, data) return createSlashOption(SlashCommandOptionType.STRING, data)
} }
@ -104,9 +127,7 @@ export class SlashOptionCallableBuilder {
} }
} }
export type SlashOptionCallable = ( export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption
o: typeof SlashOptionCallableBuilder
) => SlashCommandOption
export type SlashBuilderOptionsData = export type SlashBuilderOptionsData =
| Array<SlashCommandOption | SlashOptionCallable> | Array<SlashCommandOption | SlashOptionCallable>
@ -125,12 +146,10 @@ function buildOptionsArray(
options: SlashBuilderOptionsData options: SlashBuilderOptionsData
): SlashCommandOption[] { ): SlashCommandOption[] {
return Array.isArray(options) return Array.isArray(options)
? options.map((op) => ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op))
typeof op === 'function' ? op(SlashOptionCallableBuilder) : op
)
: Object.entries(options).map((entry) => : Object.entries(options).map((entry) =>
typeof entry[1] === 'function' typeof entry[1] === 'function'
? entry[1](SlashOptionCallableBuilder) ? entry[1](SlashOption)
: Object.assign(entry[1], { name: entry[0] }) : Object.assign(entry[1], { name: entry[0] })
) )
} }
@ -163,7 +182,7 @@ export class SlashBuilder {
option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder {
if (this.data.options === undefined) this.data.options = [] if (this.data.options === undefined) this.data.options = []
this.data.options.push( this.data.options.push(
typeof option === 'function' ? option(SlashOptionCallableBuilder) : option typeof option === 'function' ? option(SlashOption) : option
) )
return this return this
} }
@ -312,6 +331,7 @@ export interface SlashOptions {
enabled?: boolean enabled?: boolean
token?: string token?: string
rest?: RESTManager rest?: RESTManager
publicKey?: string
} }
export class SlashClient { export class SlashClient {
@ -322,6 +342,18 @@ export class SlashClient {
commands: SlashCommandsManager commands: SlashCommandsManager
handlers: SlashCommandHandler[] = [] handlers: SlashCommandHandler[] = []
rest: RESTManager 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) { constructor(options: SlashOptions) {
let id = options.id let id = options.id
@ -332,6 +364,7 @@ export class SlashClient {
this.client = options.client this.client = options.client
this.token = options.token this.token = options.token
this.commands = new SlashCommandsManager(this) this.commands = new SlashCommandsManager(this)
this.publicKey = options.publicKey
if (options !== undefined) { if (options !== undefined) {
this.enabled = options.enabled ?? true 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 = this.rest =
options.client === undefined options.client === undefined
? options.rest === undefined ? options.rest === undefined
@ -367,8 +418,16 @@ export class SlashClient {
return this 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 { 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 hasGroupOrParent = e.group !== undefined || e.parent !== undefined
const groupMatched = const groupMatched =
e.group !== undefined && e.parent !== undefined e.group !== undefined && e.parent !== undefined
@ -401,4 +460,78 @@ export class SlashClient {
cmd.handler(interaction) cmd.handler(interaction)
} }
async verifyKey(
rawBody: string | Uint8Array | Buffer,
signature: string,
timestamp: string
): Promise<boolean> {
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<boolean> {
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<any> {
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<any> {
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
}
} }