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
+ }
}