Merge pull request #67 from DjDeveloperr/slash
feat: HTTP based Slash Commands util
This commit is contained in:
		
						commit
						d46cd0cbe5
					
				
					 10 changed files with 461 additions and 65 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! | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -247,6 +247,7 @@ export function event(name?: string) { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Decorator to create a Slash Command handler */ | ||||||
| export function slash(name?: string, guild?: string) { | export function slash(name?: string, guild?: string) { | ||||||
|   return function (client: Client | SlashModule, prop: string) { |   return function (client: Client | SlashModule, prop: string) { | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|  | @ -262,6 +263,7 @@ export function slash(name?: string, guild?: string) { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Decorator to create a Sub-Slash Command handler */ | ||||||
| export function subslash(parent: string, name?: string, guild?: string) { | export function subslash(parent: string, name?: string, guild?: string) { | ||||||
|   return function (client: Client | SlashModule, prop: string) { |   return function (client: Client | SlashModule, prop: string) { | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|  | @ -279,13 +281,14 @@ export function subslash(parent: string, name?: string, guild?: string) { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Decorator to create a Grouped Slash Command handler */ | ||||||
| export function groupslash( | export function groupslash( | ||||||
|   parent: string, |   parent: string, | ||||||
|   group: string, |   group: string, | ||||||
|   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') { | ||||||
|  | @ -303,6 +306,7 @@ export function groupslash( | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Decorator to add a Slash Module to Client */ | ||||||
| export function slashModule() { | export function slashModule() { | ||||||
|   return function (client: Client, prop: string) { |   return function (client: Client, prop: string) { | ||||||
|     if (client._decoratedSlashModules === undefined) |     if (client._decoratedSlashModules === undefined) | ||||||
|  |  | ||||||
|  | @ -97,6 +97,7 @@ export interface RESTOptions { | ||||||
|   token?: string |   token?: string | ||||||
|   headers?: { [name: string]: string | undefined } |   headers?: { [name: string]: string | undefined } | ||||||
|   canary?: boolean |   canary?: boolean | ||||||
|  |   version?: 6 | 7 | 8 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class RESTManager { | export class RESTManager { | ||||||
|  | @ -111,6 +112,7 @@ export class RESTManager { | ||||||
|   constructor(client?: RESTOptions) { |   constructor(client?: RESTOptions) { | ||||||
|     this.client = client |     this.client = client | ||||||
|     this.api = builder(this) |     this.api = builder(this) | ||||||
|  |     if (client?.version !== undefined) this.version = client.version | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||||
|     this.handleRateLimits() |     this.handleRateLimits() | ||||||
|   } |   } | ||||||
|  | @ -408,6 +410,7 @@ export class RESTManager { | ||||||
|           const query = |           const query = | ||||||
|             method === 'get' && body !== undefined |             method === 'get' && body !== undefined | ||||||
|               ? Object.entries(body as any) |               ? Object.entries(body as any) | ||||||
|  |                   .filter(([k, v]) => v !== undefined) | ||||||
|                   .map( |                   .map( | ||||||
|                     ([key, value]) => |                     ([key, value]) => | ||||||
|                       `${encodeURIComponent(key)}=${encodeURIComponent( |                       `${encodeURIComponent(key)}=${encodeURIComponent( | ||||||
|  |  | ||||||
|  | @ -1,20 +1,24 @@ | ||||||
| import { Guild } from '../structures/guild.ts' | import { Guild } from '../structures/guild.ts' | ||||||
| import { Interaction } from '../structures/slash.ts' | import { Interaction } from '../structures/slash.ts' | ||||||
| import { |  | ||||||
|   APPLICATION_COMMAND, |  | ||||||
|   APPLICATION_COMMANDS, |  | ||||||
|   APPLICATION_GUILD_COMMAND, |  | ||||||
|   APPLICATION_GUILD_COMMANDS |  | ||||||
| } from '../types/endpoint.ts' |  | ||||||
| import { | import { | ||||||
|   InteractionType, |   InteractionType, | ||||||
|  |   SlashCommandChoice, | ||||||
|   SlashCommandOption, |   SlashCommandOption, | ||||||
|  |   SlashCommandOptionType, | ||||||
|   SlashCommandPartial, |   SlashCommandPartial, | ||||||
|   SlashCommandPayload |   SlashCommandPayload | ||||||
| } from '../types/slash.ts' | } from '../types/slash.ts' | ||||||
| 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 | ||||||
|  | @ -41,6 +45,158 @@ 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 { | ||||||
|  |   name: string | ||||||
|  |   description?: string | ||||||
|  |   options?: Array<SlashCommandOption | SlashOptionCallable> | ||||||
|  |   choices?: Array<SlashCommandChoice | string> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function createSlashOption( | ||||||
|  |   type: SlashCommandOptionType, | ||||||
|  |   data: CreateOptions | ||||||
|  | ): SlashCommandOption { | ||||||
|  |   return { | ||||||
|  |     name: data.name, | ||||||
|  |     type, | ||||||
|  |     description: | ||||||
|  |       type === 0 || type === 1 | ||||||
|  |         ? undefined | ||||||
|  |         : data.description ?? 'No description.', | ||||||
|  |     options: data.options?.map((e) => | ||||||
|  |       typeof e === 'function' ? e(SlashOption) : e | ||||||
|  |     ), | ||||||
|  |     choices: | ||||||
|  |       data.choices === undefined | ||||||
|  |         ? undefined | ||||||
|  |         : data.choices.map((e) => | ||||||
|  |             typeof e === 'string' ? { name: e, value: e } : e | ||||||
|  |           ) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 | ||||||
|  | export class SlashOption { | ||||||
|  |   static string(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.STRING, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static bool(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.BOOLEAN, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static subCommand(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static subCommandGroup(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static role(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.ROLE, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static channel(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.CHANNEL, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static user(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.USER, data) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static number(data: CreateOptions): SlashCommandOption { | ||||||
|  |     return createSlashOption(SlashCommandOptionType.INTEGER, data) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption | ||||||
|  | 
 | ||||||
|  | export type SlashBuilderOptionsData = | ||||||
|  |   | Array<SlashCommandOption | SlashOptionCallable> | ||||||
|  |   | { | ||||||
|  |       [name: string]: | ||||||
|  |         | { | ||||||
|  |             description: string | ||||||
|  |             type: SlashCommandOptionType | ||||||
|  |             options?: SlashCommandOption[] | ||||||
|  |             choices?: SlashCommandChoice[] | ||||||
|  |           } | ||||||
|  |         | SlashOptionCallable | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | function buildOptionsArray( | ||||||
|  |   options: SlashBuilderOptionsData | ||||||
|  | ): SlashCommandOption[] { | ||||||
|  |   return Array.isArray(options) | ||||||
|  |     ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op)) | ||||||
|  |     : Object.entries(options).map((entry) => | ||||||
|  |         typeof entry[1] === 'function' | ||||||
|  |           ? entry[1](SlashOption) | ||||||
|  |           : Object.assign(entry[1], { name: entry[0] }) | ||||||
|  |       ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class SlashBuilder { | ||||||
|  |   data: SlashCommandPartial | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     name?: string, | ||||||
|  |     description?: string, | ||||||
|  |     options?: SlashBuilderOptionsData | ||||||
|  |   ) { | ||||||
|  |     this.data = { | ||||||
|  |       name: name ?? '', | ||||||
|  |       description: description ?? 'No description.', | ||||||
|  |       options: options === undefined ? [] : buildOptionsArray(options) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   name(name: string): SlashBuilder { | ||||||
|  |     this.data.name = name | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   description(desc: string): SlashBuilder { | ||||||
|  |     this.data.description = desc | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { | ||||||
|  |     if (this.data.options === undefined) this.data.options = [] | ||||||
|  |     this.data.options.push( | ||||||
|  |       typeof option === 'function' ? option(SlashOption) : option | ||||||
|  |     ) | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   options(options: SlashBuilderOptionsData): SlashBuilder { | ||||||
|  |     this.data.options = buildOptionsArray(options) | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   export(): SlashCommandPartial { | ||||||
|  |     if (this.data.name === '') | ||||||
|  |       throw new Error('Name was not provided in Slash Builder') | ||||||
|  |     return this.data | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class SlashCommandsManager { | export class SlashCommandsManager { | ||||||
|  | @ -58,9 +214,9 @@ export class SlashCommandsManager { | ||||||
|   async all(): Promise<Collection<string, SlashCommand>> { |   async all(): Promise<Collection<string, SlashCommand>> { | ||||||
|     const col = new Collection<string, SlashCommand>() |     const col = new Collection<string, SlashCommand>() | ||||||
| 
 | 
 | ||||||
|     const res = (await this.rest.get( |     const res = (await this.rest.api.applications[ | ||||||
|       APPLICATION_COMMANDS(this.slash.getID()) |       this.slash.getID() | ||||||
|     )) as SlashCommandPayload[] |     ].commands.get()) as SlashCommandPayload[] | ||||||
|     if (!Array.isArray(res)) return col |     if (!Array.isArray(res)) return col | ||||||
| 
 | 
 | ||||||
|     for (const raw of res) { |     for (const raw of res) { | ||||||
|  | @ -77,12 +233,9 @@ export class SlashCommandsManager { | ||||||
|   ): Promise<Collection<string, SlashCommand>> { |   ): Promise<Collection<string, SlashCommand>> { | ||||||
|     const col = new Collection<string, SlashCommand>() |     const col = new Collection<string, SlashCommand>() | ||||||
| 
 | 
 | ||||||
|     const res = (await this.rest.get( |     const res = (await this.rest.api.applications[this.slash.getID()].guilds[ | ||||||
|       APPLICATION_GUILD_COMMANDS( |  | ||||||
|         this.slash.getID(), |  | ||||||
|       typeof guild === 'string' ? guild : guild.id |       typeof guild === 'string' ? guild : guild.id | ||||||
|       ) |     ].commands.get()) as SlashCommandPayload[] | ||||||
|     )) as SlashCommandPayload[] |  | ||||||
|     if (!Array.isArray(res)) return col |     if (!Array.isArray(res)) return col | ||||||
| 
 | 
 | ||||||
|     for (const raw of res) { |     for (const raw of res) { | ||||||
|  | @ -99,15 +252,14 @@ export class SlashCommandsManager { | ||||||
|     data: SlashCommandPartial, |     data: SlashCommandPartial, | ||||||
|     guild?: Guild | string |     guild?: Guild | string | ||||||
|   ): Promise<SlashCommand> { |   ): Promise<SlashCommand> { | ||||||
|     const payload = await this.rest.post( |     const route = | ||||||
|       guild === undefined |       guild === undefined | ||||||
|         ? APPLICATION_COMMANDS(this.slash.getID()) |         ? this.rest.api.applications[this.slash.getID()].commands | ||||||
|         : APPLICATION_GUILD_COMMANDS( |         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||||
|             this.slash.getID(), |  | ||||||
|             typeof guild === 'string' ? guild : guild.id |             typeof guild === 'string' ? guild : guild.id | ||||||
|           ), |           ].commands | ||||||
|       data | 
 | ||||||
|     ) |     const payload = await route.post(data) | ||||||
| 
 | 
 | ||||||
|     const cmd = new SlashCommand(this, payload) |     const cmd = new SlashCommand(this, payload) | ||||||
|     cmd._guild = |     cmd._guild = | ||||||
|  | @ -122,16 +274,14 @@ export class SlashCommandsManager { | ||||||
|     data: SlashCommandPartial, |     data: SlashCommandPartial, | ||||||
|     guild?: Guild | string |     guild?: Guild | string | ||||||
|   ): Promise<SlashCommandsManager> { |   ): Promise<SlashCommandsManager> { | ||||||
|     await this.rest.patch( |     const route = | ||||||
|       guild === undefined |       guild === undefined | ||||||
|         ? APPLICATION_COMMAND(this.slash.getID(), id) |         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||||
|         : APPLICATION_GUILD_COMMAND( |         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||||
|             this.slash.getID(), |             typeof guild === 'string' ? guild : guild.id | ||||||
|             typeof guild === 'string' ? guild : guild.id, |           ].commands[id] | ||||||
|             id | 
 | ||||||
|           ), |     await route.patch(data) | ||||||
|       data |  | ||||||
|     ) |  | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -140,29 +290,28 @@ export class SlashCommandsManager { | ||||||
|     id: string, |     id: string, | ||||||
|     guild?: Guild | string |     guild?: Guild | string | ||||||
|   ): Promise<SlashCommandsManager> { |   ): Promise<SlashCommandsManager> { | ||||||
|     await this.rest.delete( |     const route = | ||||||
|       guild === undefined |       guild === undefined | ||||||
|         ? APPLICATION_COMMAND(this.slash.getID(), id) |         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||||
|         : APPLICATION_GUILD_COMMAND( |         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||||
|             this.slash.getID(), |             typeof guild === 'string' ? guild : guild.id | ||||||
|             typeof guild === 'string' ? guild : guild.id, |           ].commands[id] | ||||||
|             id | 
 | ||||||
|           ) |     await route.delete() | ||||||
|     ) |  | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Get a Slash Command (global or Guild) */ |   /** Get a Slash Command (global or Guild) */ | ||||||
|   async get(id: string, guild?: Guild | string): Promise<SlashCommand> { |   async get(id: string, guild?: Guild | string): Promise<SlashCommand> { | ||||||
|     const data = await this.rest.get( |     const route = | ||||||
|       guild === undefined |       guild === undefined | ||||||
|         ? APPLICATION_COMMAND(this.slash.getID(), id) |         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||||
|         : APPLICATION_GUILD_COMMAND( |         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||||
|             this.slash.getID(), |             typeof guild === 'string' ? guild : guild.id | ||||||
|             typeof guild === 'string' ? guild : guild.id, |           ].commands[id] | ||||||
|             id | 
 | ||||||
|           ) |     const data = await route.get() | ||||||
|     ) | 
 | ||||||
|     return new SlashCommand(this, data) |     return new SlashCommand(this, data) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | @ -182,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 { | ||||||
|  | @ -192,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 | ||||||
|  | @ -202,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 | ||||||
|  | @ -213,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 | ||||||
|  | @ -237,8 +418,28 @@ export class SlashClient { | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   loadModule(module: SlashModule): SlashClient { | ||||||
|  |     this.modules.push(module) | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getHandlers(): SlashCommandHandler[] { | ||||||
|  |     let res = this.handlers | ||||||
|  |     for (const mod of this.modules) { | ||||||
|  |       if (mod === undefined) continue | ||||||
|  |       res = [ | ||||||
|  |         ...res, | ||||||
|  |         ...mod.commands.map((cmd) => { | ||||||
|  |           cmd.handler = cmd.handler.bind(mod) | ||||||
|  |           return cmd | ||||||
|  |         }) | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |     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 | ||||||
|  | @ -271,4 +472,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 | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -46,6 +46,10 @@ export class Message extends Base { | ||||||
|   flags?: number |   flags?: number | ||||||
|   stickers?: MessageSticker[] |   stickers?: MessageSticker[] | ||||||
| 
 | 
 | ||||||
|  |   get createdAt(): Date { | ||||||
|  |     return new Date(this.timestamp) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     client: Client, |     client: Client, | ||||||
|     data: MessagePayload, |     data: MessagePayload, | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { Client } from '../models/client.ts' | ||||||
| import { | import { | ||||||
|   GuildTextChannelPayload, |   GuildTextChannelPayload, | ||||||
|   MessageOption, |   MessageOption, | ||||||
|  |   MessagePayload, | ||||||
|   MessageReference, |   MessageReference, | ||||||
|   ModifyGuildTextChannelOption, |   ModifyGuildTextChannelOption, | ||||||
|   ModifyGuildTextChannelPayload, |   ModifyGuildTextChannelPayload, | ||||||
|  | @ -14,6 +15,7 @@ import { | ||||||
|   CHANNEL_MESSAGE, |   CHANNEL_MESSAGE, | ||||||
|   CHANNEL_MESSAGES |   CHANNEL_MESSAGES | ||||||
| } from '../types/endpoint.ts' | } from '../types/endpoint.ts' | ||||||
|  | import { Collection } from '../utils/collection.ts' | ||||||
| import { Channel } from './channel.ts' | import { Channel } from './channel.ts' | ||||||
| import { Embed } from './embed.ts' | import { Embed } from './embed.ts' | ||||||
| import { Guild } from './guild.ts' | import { Guild } from './guild.ts' | ||||||
|  | @ -125,6 +127,48 @@ export class TextChannel extends Channel { | ||||||
|     await res.mentions.fromPayload(newMsg) |     await res.mentions.fromPayload(newMsg) | ||||||
|     return res |     return res | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Fetch Messages of a Channel | ||||||
|  |    * @param options Options to configure fetching Messages | ||||||
|  |    */ | ||||||
|  |   async fetchMessages(options?: { | ||||||
|  |     limit?: number | ||||||
|  |     around?: Message | string | ||||||
|  |     before?: Message | string | ||||||
|  |     after?: Message | string | ||||||
|  |   }): Promise<Collection<string, Message>> { | ||||||
|  |     const res = new Collection<string, Message>() | ||||||
|  |     const raws = (await this.client.rest.api.channels[this.id].messages.get({ | ||||||
|  |       limit: options?.limit ?? 50, | ||||||
|  |       around: | ||||||
|  |         options?.around === undefined | ||||||
|  |           ? undefined | ||||||
|  |           : typeof options.around === 'string' | ||||||
|  |           ? options.around | ||||||
|  |           : options.around.id, | ||||||
|  |       before: | ||||||
|  |         options?.before === undefined | ||||||
|  |           ? undefined | ||||||
|  |           : typeof options.before === 'string' | ||||||
|  |           ? options.before | ||||||
|  |           : options.before.id, | ||||||
|  |       after: | ||||||
|  |         options?.after === undefined | ||||||
|  |           ? undefined | ||||||
|  |           : typeof options.after === 'string' | ||||||
|  |           ? options.after | ||||||
|  |           : options.after.id | ||||||
|  |     })) as MessagePayload[] | ||||||
|  | 
 | ||||||
|  |     for (const raw of raws) { | ||||||
|  |       await this.messages.set(raw.id, raw) | ||||||
|  |       const msg = ((await this.messages.get(raw.id)) as unknown) as Message | ||||||
|  |       res.set(msg.id, msg) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return res | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class GuildTextChannel extends TextChannel { | export class GuildTextChannel extends TextChannel { | ||||||
|  | @ -186,4 +230,40 @@ export class GuildTextChannel extends TextChannel { | ||||||
| 
 | 
 | ||||||
|     return new GuildTextChannel(this.client, resp, this.guild) |     return new GuildTextChannel(this.client, resp, this.guild) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Bulk Delete Messages in a Guild Text Channel | ||||||
|  |    * @param messages Messages to delete. Can be a number, or Array of Message or IDs | ||||||
|  |    */ | ||||||
|  |   async bulkDelete( | ||||||
|  |     messages: Array<Message | string> | number | ||||||
|  |   ): Promise<GuildTextChannel> { | ||||||
|  |     let ids: string[] = [] | ||||||
|  | 
 | ||||||
|  |     if (Array.isArray(messages)) | ||||||
|  |       ids = messages.map((e) => (typeof e === 'string' ? e : e.id)) | ||||||
|  |     else { | ||||||
|  |       let list = await this.messages.array() | ||||||
|  |       if (list.length < messages) list = (await this.fetchMessages()).array() | ||||||
|  |       ids = list | ||||||
|  |         .sort((b, a) => a.createdAt.getTime() - b.createdAt.getTime()) | ||||||
|  |         .filter((e, i) => i < messages) | ||||||
|  |         .filter( | ||||||
|  |           (e) => | ||||||
|  |             new Date().getTime() - e.createdAt.getTime() <= | ||||||
|  |             1000 * 60 * 60 * 24 * 14 | ||||||
|  |         ) | ||||||
|  |         .map((e) => e.id) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ids = [...new Set(ids)] | ||||||
|  |     if (ids.length < 2 || ids.length > 100) | ||||||
|  |       throw new Error('bulkDelete can only delete messages in range 2-100') | ||||||
|  | 
 | ||||||
|  |     await this.client.rest.api.channels[this.id].messages['bulk-delete'].post({ | ||||||
|  |       messages: ids | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     return this | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,7 +7,8 @@ import { | ||||||
|   groupslash, |   groupslash, | ||||||
|   CommandContext, |   CommandContext, | ||||||
|   Extension, |   Extension, | ||||||
|   Collection |   Collection, | ||||||
|  |   GuildTextChannel | ||||||
| } from '../../mod.ts' | } from '../../mod.ts' | ||||||
| import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' | import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' | ||||||
| import { | import { | ||||||
|  | @ -69,6 +70,17 @@ class MyClient extends CommandClient { | ||||||
|     d.respond({ content: 'sub-cmd-group worked' }) |     d.respond({ content: 'sub-cmd-group worked' }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @command() | ||||||
|  |   rmrf(ctx: CommandContext): any { | ||||||
|  |     if (ctx.author.id !== '422957901716652033') return | ||||||
|  |     ;((ctx.channel as any) as GuildTextChannel) | ||||||
|  |       .bulkDelete(3) | ||||||
|  |       .then((chan) => { | ||||||
|  |         ctx.channel.send(`Bulk deleted 2 in ${chan}`) | ||||||
|  |       }) | ||||||
|  |       .catch((e) => ctx.channel.send(`${e.message}`)) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @slash() |   @slash() | ||||||
|   run(d: Interaction): void { |   run(d: Interaction): void { | ||||||
|     console.log(d.name) |     console.log(d.name) | ||||||
|  | @ -205,6 +217,10 @@ class VCExtension extends Extension { | ||||||
| 
 | 
 | ||||||
| const client = new MyClient() | const client = new MyClient() | ||||||
| 
 | 
 | ||||||
|  | client.on('raw', (e, d) => { | ||||||
|  |   if (e === 'GUILD_MEMBER_ADD' || e === 'GUILD_MEMBER_UPDATE') console.log(e, d) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
| client.extensions.load(VCExtension) | client.extensions.load(VCExtension) | ||||||
| 
 | 
 | ||||||
| client.connect(TOKEN, Intents.None) | client.connect(TOKEN, Intents.All) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,16 @@ | ||||||
| import { SlashClient } from '../models/slashClient.ts' | import { SlashClient } from '../models/slashClient.ts' | ||||||
|  | import { SlashCommandPartial } from '../types/slash.ts' | ||||||
| import { TOKEN } from './config.ts' | import { TOKEN } from './config.ts' | ||||||
| 
 | 
 | ||||||
| const slash = new SlashClient({ token: TOKEN }) | export const slash = new SlashClient({ token: TOKEN }) | ||||||
| 
 | 
 | ||||||
| slash.commands.all().then(console.log) | // Cmd objects come here
 | ||||||
|  | const commands: SlashCommandPartial[] = [] | ||||||
|  | 
 | ||||||
|  | console.log('Creating...') | ||||||
|  | commands.forEach((cmd) => { | ||||||
|  |   slash.commands | ||||||
|  |     .create(cmd, '!! Your testing guild ID comes here !!') | ||||||
|  |     .then((c) => console.log(`Created command ${c.name}!`)) | ||||||
|  |     .catch((e) => `Failed to create ${cmd.name} - ${e.message}`) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | @ -62,6 +62,7 @@ export interface MemberPayload { | ||||||
|   premium_since?: string |   premium_since?: string | ||||||
|   deaf: boolean |   deaf: boolean | ||||||
|   mute: boolean |   mute: boolean | ||||||
|  |   pending?: boolean | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum MessageNotification { | export enum MessageNotification { | ||||||
|  | @ -113,6 +114,9 @@ export type GuildFeatures = | ||||||
|   | 'FEATURABLE' |   | 'FEATURABLE' | ||||||
|   | 'ANIMATED_ICON' |   | 'ANIMATED_ICON' | ||||||
|   | 'BANNER' |   | 'BANNER' | ||||||
|  |   | 'WELCOME_SCREEN_ENABLED' | ||||||
|  |   | 'MEMBER_VERIFICATION_GATE_ENABLED' | ||||||
|  |   | 'PREVIEW_ENABLED' | ||||||
| 
 | 
 | ||||||
| export enum IntegrationExpireBehavior { | export enum IntegrationExpireBehavior { | ||||||
|   REMOVE_ROLE = 0, |   REMOVE_ROLE = 0, | ||||||
|  |  | ||||||
|  | @ -50,7 +50,7 @@ export interface SlashCommandChoice { | ||||||
|   /** (Display) name of the Choice */ |   /** (Display) name of the Choice */ | ||||||
|   name: string |   name: string | ||||||
|   /** Actual value to be sent in Interaction */ |   /** Actual value to be sent in Interaction */ | ||||||
|   value: string |   value: any | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum SlashCommandOptionType { | export enum SlashCommandOptionType { | ||||||
|  | @ -66,7 +66,8 @@ export enum SlashCommandOptionType { | ||||||
| 
 | 
 | ||||||
| export interface SlashCommandOption { | export interface SlashCommandOption { | ||||||
|   name: string |   name: string | ||||||
|   description: string |   /** Description not required in Sub-Command or Sub-Command-Group */ | ||||||
|  |   description?: string | ||||||
|   type: SlashCommandOptionType |   type: SlashCommandOptionType | ||||||
|   required?: boolean |   required?: boolean | ||||||
|   default?: boolean |   default?: boolean | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue