feat: middlewares and http-based slash util
This commit is contained in:
		
							parent
							
								
									da0bfc12c7
								
							
						
					
					
						commit
						03ea5df551
					
				
					 3 changed files with 154 additions and 22 deletions
				
			
		
							
								
								
									
										19
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								README.md
									
										
									
									
									
								
							|  | @ -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! | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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') { | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue