fix
This commit is contained in:
		
						commit
						a1ead7e15e
					
				
					 169 changed files with 4849 additions and 2446 deletions
				
			
		
							
								
								
									
										4
									
								
								.github/workflows/deno.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/deno.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -20,14 +20,14 @@ jobs: | ||||||
| 
 | 
 | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         deno: ['v1.x', 'nightly'] |         deno: ['v1.x', 'canary'] | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|       - name: Setup repo |       - name: Setup repo | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
| 
 | 
 | ||||||
|       - name: Setup Deno |       - name: Setup Deno | ||||||
|         uses: denolib/setup-deno@v2.3.0 |         uses: denoland/setup-deno@main | ||||||
|         with: |         with: | ||||||
|           deno-version: ${{ matrix.deno }} # tests across multiple Deno versions |           deno-version: ${{ matrix.deno }} # tests across multiple Deno versions | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -109,6 +109,7 @@ yarn.lock | ||||||
| 
 | 
 | ||||||
| # PRIVACY XDDDD | # PRIVACY XDDDD | ||||||
| src/test/config.ts | src/test/config.ts | ||||||
|  | test/config.ts | ||||||
| .vscode | .vscode | ||||||
| 
 | 
 | ||||||
| # macOS is shit xD | # macOS is shit xD | ||||||
|  | @ -117,4 +118,4 @@ src/test/config.ts | ||||||
| # Webstorm dont forget this duude :) | # Webstorm dont forget this duude :) | ||||||
| .idea/ | .idea/ | ||||||
| 
 | 
 | ||||||
| src/test/music.mp3 | src/test/music.mp3 | ||||||
|  | @ -59,7 +59,7 @@ client.on('ready', () => { | ||||||
| // Listen for event whenever a Message is sent | // Listen for event whenever a Message is sent | ||||||
| client.on('messageCreate', (msg: Message): void => { | client.on('messageCreate', (msg: Message): void => { | ||||||
|   if (msg.content === '!ping') { |   if (msg.content === '!ping') { | ||||||
|     msg.channel.send(`Pong! WS Ping: ${client.ping}`) |     msg.channel.send(`Pong! WS Ping: ${client.gateway.ping}`) | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
|  | @ -95,7 +95,7 @@ class PingCommand extends Command { | ||||||
|   name = 'ping' |   name = 'ping' | ||||||
| 
 | 
 | ||||||
|   execute(ctx: CommandContext) { |   execute(ctx: CommandContext) { | ||||||
|     ctx.message.reply(`pong! Ping: ${ctx.client.ping}ms`) |     ctx.message.reply(`pong! Ping: ${ctx.client.gateway.ping}ms`) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -156,7 +156,7 @@ Documentation is available for `main` (branch) and `stable` (release). | ||||||
| 
 | 
 | ||||||
| ## Found a bug or want support? Join our discord server! | ## Found a bug or want support? Join our discord server! | ||||||
| 
 | 
 | ||||||
| [](https://discord.gg/WVN2JF2FRv) | [](https://discord.gg/harmonyland) | ||||||
| 
 | 
 | ||||||
| ## Maintainer | ## Maintainer | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										131
									
								
								deploy.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								deploy.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,131 @@ | ||||||
|  | import { | ||||||
|  |   SlashCommandsManager, | ||||||
|  |   SlashClient, | ||||||
|  |   SlashCommandHandlerCallback, | ||||||
|  |   SlashCommandHandler | ||||||
|  | } from './src/interactions/mod.ts' | ||||||
|  | import { InteractionResponseType, InteractionType } from './src/types/slash.ts' | ||||||
|  | 
 | ||||||
|  | export interface DeploySlashInitOptions { | ||||||
|  |   env?: boolean | ||||||
|  |   publicKey?: string | ||||||
|  |   token?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Current Slash Client being used to handle commands */ | ||||||
|  | let client: SlashClient | ||||||
|  | /** Manage Slash Commands right in Deploy */ | ||||||
|  | let commands: SlashCommandsManager | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Initialize Slash Commands Handler for [Deno Deploy](https://deno.com/deploy).
 | ||||||
|  |  * Easily create Serverless Slash Commands on the fly. | ||||||
|  |  * | ||||||
|  |  * **Examples** | ||||||
|  |  * | ||||||
|  |  * ```ts
 | ||||||
|  |  * init({ | ||||||
|  |  *   publicKey: "my public key", | ||||||
|  |  *   token: "my bot's token", // only required if you want to manage slash commands in code
 | ||||||
|  |  * }) | ||||||
|  |  * ``` | ||||||
|  |  * | ||||||
|  |  * ```ts
 | ||||||
|  |  * // takes up `PUBLIC_KEY` and `TOKEN` from ENV
 | ||||||
|  |  * init({ env: true }) | ||||||
|  |  * ``` | ||||||
|  |  * | ||||||
|  |  * @param options Initialization options | ||||||
|  |  */ | ||||||
|  | export function init(options: { env: boolean }): void | ||||||
|  | export function init(options: { publicKey: string; token?: string }): void | ||||||
|  | export function init(options: DeploySlashInitOptions): void { | ||||||
|  |   if (client !== undefined) throw new Error('Already initialized') | ||||||
|  |   if (options.env === true) { | ||||||
|  |     options.publicKey = Deno.env.get('PUBLIC_KEY') | ||||||
|  |     options.token = Deno.env.get('TOKEN') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (options.publicKey === undefined) | ||||||
|  |     throw new Error('Public Key not provided') | ||||||
|  | 
 | ||||||
|  |   client = new SlashClient({ | ||||||
|  |     token: options.token, | ||||||
|  |     publicKey: options.publicKey | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   commands = client.commands | ||||||
|  | 
 | ||||||
|  |   const cb = async (evt: { | ||||||
|  |     respondWith: CallableFunction | ||||||
|  |     request: Request | ||||||
|  |   }): Promise<void> => { | ||||||
|  |     try { | ||||||
|  |       // we have to wrap because there are some weird scope errors
 | ||||||
|  |       const d = await client.verifyFetchEvent({ | ||||||
|  |         respondWith: (...args: any[]) => evt.respondWith(...args), | ||||||
|  |         request: evt.request | ||||||
|  |       }) | ||||||
|  |       if (d === false) { | ||||||
|  |         await evt.respondWith( | ||||||
|  |           new Response('Not Authorized', { | ||||||
|  |             status: 400 | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (d.type === InteractionType.PING) { | ||||||
|  |         await d.respond({ type: InteractionResponseType.PONG }) | ||||||
|  |         client.emit('ping') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       await (client as any)._process(d) | ||||||
|  |     } catch (e) { | ||||||
|  |       await client.emit('interactionError', e) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   addEventListener('fetch', cb as any) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Register Slash Command handler. | ||||||
|  |  * | ||||||
|  |  * Example: | ||||||
|  |  * | ||||||
|  |  * ```ts
 | ||||||
|  |  * handle("ping", (interaction) => { | ||||||
|  |  *   interaction.reply("Pong!") | ||||||
|  |  * }) | ||||||
|  |  * ``` | ||||||
|  |  * | ||||||
|  |  * Also supports Sub Command and Group handling out of the box! | ||||||
|  |  * ```ts
 | ||||||
|  |  * handle("command-name group-name sub-command", (i) => { | ||||||
|  |  *   // ...
 | ||||||
|  |  * }) | ||||||
|  |  * | ||||||
|  |  * handle("command-name sub-command", (i) => { | ||||||
|  |  *   // ...
 | ||||||
|  |  * }) | ||||||
|  |  * ``` | ||||||
|  |  * | ||||||
|  |  * @param cmd Command to handle. Either Handler object or command name followed by handler function in next parameter. | ||||||
|  |  * @param handler Handler function (required if previous argument was command name) | ||||||
|  |  */ | ||||||
|  | export function handle( | ||||||
|  |   cmd: string | SlashCommandHandler, | ||||||
|  |   handler?: SlashCommandHandlerCallback | ||||||
|  | ): void { | ||||||
|  |   if (client === undefined) | ||||||
|  |     throw new Error('Slash Client not initialized. Call `init` first') | ||||||
|  |   client.handle(cmd, handler) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { commands, client } | ||||||
|  | export * from './src/types/slash.ts' | ||||||
|  | export * from './src/structures/slash.ts' | ||||||
|  | export * from './src/interactions/mod.ts' | ||||||
|  | export * from './src/types/channel.ts' | ||||||
							
								
								
									
										5
									
								
								deps.ts
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								deps.ts
									
										
									
									
									
								
							|  | @ -1,11 +1,6 @@ | ||||||
| export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' | export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' | ||||||
| export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' | export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' | ||||||
| export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts' | export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts' | ||||||
| export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts' |  | ||||||
| export type { |  | ||||||
|   Redis, |  | ||||||
|   RedisConnectOptions |  | ||||||
| } from 'https://deno.land/x/redis@v0.14.1/mod.ts' |  | ||||||
| export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts' | export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts' | ||||||
| export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' | export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' | ||||||
| export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' | export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								mod.ts
									
										
									
									
									
								
							
							
						
						
									
										73
									
								
								mod.ts
									
										
									
									
									
								
							|  | @ -1,14 +1,18 @@ | ||||||
| export { GatewayIntents } from './src/types/gateway.ts' | export { GatewayIntents } from './src/types/gateway.ts' | ||||||
| export { Base } from './src/structures/base.ts' | export { Base } from './src/structures/base.ts' | ||||||
| export { Gateway } from './src/gateway/index.ts' | export { Gateway } from './src/gateway/mod.ts' | ||||||
| export type { GatewayTypedEvents } from './src/gateway/index.ts' | export type { GatewayTypedEvents } from './src/gateway/mod.ts' | ||||||
| export type { ClientEvents } from './src/gateway/handlers/index.ts' | export type { ClientEvents } from './src/gateway/handlers/mod.ts' | ||||||
| export * from './src/models/client.ts' | export * from './src/client/mod.ts' | ||||||
| export * from './src/models/slashClient.ts' | export * from './src/interactions/mod.ts' | ||||||
| export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts' | export { | ||||||
| export type { RequestHeaders } from './src/models/rest.ts' |   RESTManager, | ||||||
| export type { RESTOptions } from './src/models/rest.ts' |   TokenType, | ||||||
| export * from './src/models/cacheAdapter.ts' |   HttpResponseCode, | ||||||
|  |   DiscordAPIError | ||||||
|  | } from './src/rest/mod.ts' | ||||||
|  | export * from './src/rest/mod.ts' | ||||||
|  | export * from './src/cache/adapter.ts' | ||||||
| export { | export { | ||||||
|   Command, |   Command, | ||||||
|   CommandBuilder, |   CommandBuilder, | ||||||
|  | @ -16,16 +20,16 @@ export { | ||||||
|   CommandsManager, |   CommandsManager, | ||||||
|   CategoriesManager, |   CategoriesManager, | ||||||
|   CommandsLoader |   CommandsLoader | ||||||
| } from './src/models/command.ts' | } from './src/commands/command.ts' | ||||||
| export type { CommandContext, CommandOptions } from './src/models/command.ts' | export type { CommandContext, CommandOptions } from './src/commands/command.ts' | ||||||
| export { | export { | ||||||
|   Extension, |   Extension, | ||||||
|   ExtensionCommands, |   ExtensionCommands, | ||||||
|   ExtensionsManager |   ExtensionsManager | ||||||
| } from './src/models/extensions.ts' | } from './src/commands/extension.ts' | ||||||
| export { SlashModule } from './src/models/slashModule.ts' | export { SlashModule } from './src/interactions/slashModule.ts' | ||||||
| export { CommandClient, command } from './src/models/commandClient.ts' | export { CommandClient, command } from './src/commands/client.ts' | ||||||
| export type { CommandClientOptions } from './src/models/commandClient.ts' | export type { CommandClientOptions } from './src/commands/client.ts' | ||||||
| export { BaseManager } from './src/managers/base.ts' | export { BaseManager } from './src/managers/base.ts' | ||||||
| export { BaseChildManager } from './src/managers/baseChild.ts' | export { BaseChildManager } from './src/managers/baseChild.ts' | ||||||
| export { ChannelsManager } from './src/managers/channels.ts' | export { ChannelsManager } from './src/managers/channels.ts' | ||||||
|  | @ -45,7 +49,7 @@ export { RolesManager } from './src/managers/roles.ts' | ||||||
| export { UsersManager } from './src/managers/users.ts' | export { UsersManager } from './src/managers/users.ts' | ||||||
| export { InviteManager } from './src/managers/invites.ts' | export { InviteManager } from './src/managers/invites.ts' | ||||||
| export { Application } from './src/structures/application.ts' | export { Application } from './src/structures/application.ts' | ||||||
| // export { ImageURL } from './src/structures/cdn.ts'
 | export { ImageURL } from './src/structures/cdn.ts' | ||||||
| export { Channel, GuildChannel } from './src/structures/channel.ts' | export { Channel, GuildChannel } from './src/structures/channel.ts' | ||||||
| export type { EditOverwriteOptions } from './src/structures/channel.ts' | export type { EditOverwriteOptions } from './src/structures/channel.ts' | ||||||
| export { DMChannel } from './src/structures/dmChannel.ts' | export { DMChannel } from './src/structures/dmChannel.ts' | ||||||
|  | @ -63,7 +67,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts' | ||||||
| export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' | export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' | ||||||
| export { Invite } from './src/structures/invite.ts' | export { Invite } from './src/structures/invite.ts' | ||||||
| export * from './src/structures/member.ts' | export * from './src/structures/member.ts' | ||||||
| export { Message, MessageAttachment } from './src/structures/message.ts' | export { | ||||||
|  |   Message, | ||||||
|  |   MessageAttachment, | ||||||
|  |   MessageInteraction | ||||||
|  | } from './src/structures/message.ts' | ||||||
| export { MessageMentions } from './src/structures/messageMentions.ts' | export { MessageMentions } from './src/structures/messageMentions.ts' | ||||||
| export { | export { | ||||||
|   Presence, |   Presence, | ||||||
|  | @ -88,7 +96,7 @@ export { Intents } from './src/utils/intents.ts' | ||||||
| export * from './src/utils/permissions.ts' | export * from './src/utils/permissions.ts' | ||||||
| export { UserFlagsManager } from './src/utils/userFlags.ts' | export { UserFlagsManager } from './src/utils/userFlags.ts' | ||||||
| export { HarmonyEventEmitter } from './src/utils/events.ts' | export { HarmonyEventEmitter } from './src/utils/events.ts' | ||||||
| export type { EveryChannelTypes } from './src/utils/getChannelByType.ts' | export type { EveryChannelTypes } from './src/utils/channel.ts' | ||||||
| export * from './src/utils/bitfield.ts' | export * from './src/utils/bitfield.ts' | ||||||
| export type { | export type { | ||||||
|   ActivityGame, |   ActivityGame, | ||||||
|  | @ -96,7 +104,15 @@ export type { | ||||||
|   ClientStatus, |   ClientStatus, | ||||||
|   StatusType |   StatusType | ||||||
| } from './src/types/presence.ts' | } from './src/types/presence.ts' | ||||||
| export { ChannelTypes } from './src/types/channel.ts' | export { | ||||||
|  |   ChannelTypes, | ||||||
|  |   OverwriteType, | ||||||
|  |   OverrideType | ||||||
|  | } from './src/types/channel.ts' | ||||||
|  | export type { | ||||||
|  |   OverwriteAsOptions, | ||||||
|  |   OverwritePayload | ||||||
|  | } from './src/types/channel.ts' | ||||||
| export type { ApplicationPayload } from './src/types/application.ts' | export type { ApplicationPayload } from './src/types/application.ts' | ||||||
| export type { ImageFormats, ImageSize } from './src/types/cdn.ts' | export type { ImageFormats, ImageSize } from './src/types/cdn.ts' | ||||||
| export type { | export type { | ||||||
|  | @ -110,9 +126,18 @@ export type { | ||||||
|   GuildVoiceChannelPayload, |   GuildVoiceChannelPayload, | ||||||
|   GroupDMChannelPayload, |   GroupDMChannelPayload, | ||||||
|   MessageOptions, |   MessageOptions, | ||||||
|  |   MessagePayload, | ||||||
|  |   MessageInteractionPayload, | ||||||
|  |   MessageReference, | ||||||
|  |   MessageActivity, | ||||||
|  |   MessageActivityTypes, | ||||||
|  |   MessageApplication, | ||||||
|  |   MessageFlags, | ||||||
|  |   MessageStickerFormatTypes, | ||||||
|  |   MessageStickerPayload, | ||||||
|  |   MessageTypes, | ||||||
|   OverwriteAsArg, |   OverwriteAsArg, | ||||||
|   Overwrite, |   Overwrite | ||||||
|   OverwriteAsOptions |  | ||||||
| } from './src/types/channel.ts' | } from './src/types/channel.ts' | ||||||
| export type { EmojiPayload } from './src/types/emoji.ts' | export type { EmojiPayload } from './src/types/emoji.ts' | ||||||
| export { Verification } from './src/types/guild.ts' | export { Verification } from './src/types/guild.ts' | ||||||
|  | @ -145,7 +170,9 @@ export type { UserPayload } from './src/types/user.ts' | ||||||
| export { UserFlags } from './src/types/userFlags.ts' | export { UserFlags } from './src/types/userFlags.ts' | ||||||
| export type { VoiceStatePayload } from './src/types/voice.ts' | export type { VoiceStatePayload } from './src/types/voice.ts' | ||||||
| export type { WebhookPayload } from './src/types/webhook.ts' | export type { WebhookPayload } from './src/types/webhook.ts' | ||||||
| export * from './src/models/collectors.ts' | export * from './src/client/collectors.ts' | ||||||
|  | export type { Dict } from './src/utils/dict.ts' | ||||||
|  | export * from './src/cache/redis.ts' | ||||||
| export { ColorUtil } from './src/utils/colorutil.ts' | export { ColorUtil } from './src/utils/colorutil.ts' | ||||||
| export type { Colors } from './src/utils/colorutil.ts' | export type { Colors } from './src/utils/colorutil.ts' | ||||||
| export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts' | export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts' | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								src/cache/adapter.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/cache/adapter.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | /** | ||||||
|  |  * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. | ||||||
|  |  * | ||||||
|  |  * Methods can return Promises too. | ||||||
|  |  */ | ||||||
|  | export interface ICacheAdapter { | ||||||
|  |   /** Gets a key from a Cache */ | ||||||
|  |   get: (cacheName: string, key: string) => Promise<any> | any | ||||||
|  |   /** Sets a key to value in a Cache Name with optional expire value in MS */ | ||||||
|  |   set: ( | ||||||
|  |     cacheName: string, | ||||||
|  |     key: string, | ||||||
|  |     value: any, | ||||||
|  |     expire?: number | ||||||
|  |   ) => Promise<any> | any | ||||||
|  |   /** Deletes a key from a Cache */ | ||||||
|  |   delete: (cacheName: string, key: string) => Promise<boolean> | boolean | ||||||
|  |   /** Gets array of all values in a Cache */ | ||||||
|  |   array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> | ||||||
|  |   /** Entirely deletes a Cache */ | ||||||
|  |   deleteCache: (cacheName: string) => any | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								src/cache/default.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/cache/default.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | import { Collection } from '../utils/collection.ts' | ||||||
|  | import type { ICacheAdapter } from './adapter.ts' | ||||||
|  | 
 | ||||||
|  | /** Default Cache Adapter for in-memory caching. */ | ||||||
|  | export class DefaultCacheAdapter implements ICacheAdapter { | ||||||
|  |   data: { | ||||||
|  |     [name: string]: Collection<string, any> | ||||||
|  |   } = {} | ||||||
|  | 
 | ||||||
|  |   async get(cacheName: string, key: string): Promise<undefined | any> { | ||||||
|  |     const cache = this.data[cacheName] | ||||||
|  |     if (cache === undefined) return | ||||||
|  |     return cache.get(key) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async set( | ||||||
|  |     cacheName: string, | ||||||
|  |     key: string, | ||||||
|  |     value: any, | ||||||
|  |     expire?: number | ||||||
|  |   ): Promise<any> { | ||||||
|  |     let cache = this.data[cacheName] | ||||||
|  |     if (cache === undefined) { | ||||||
|  |       this.data[cacheName] = new Collection() | ||||||
|  |       cache = this.data[cacheName] | ||||||
|  |     } | ||||||
|  |     cache.set(key, value) | ||||||
|  |     if (expire !== undefined) | ||||||
|  |       setTimeout(() => { | ||||||
|  |         cache.delete(key) | ||||||
|  |       }, expire) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async delete(cacheName: string, key: string): Promise<boolean> { | ||||||
|  |     const cache = this.data[cacheName] | ||||||
|  |     if (cache === undefined) return false | ||||||
|  |     return cache.delete(key) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async array(cacheName: string): Promise<any[] | undefined> { | ||||||
|  |     const cache = this.data[cacheName] | ||||||
|  |     if (cache === undefined) return | ||||||
|  |     return cache.array() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async deleteCache(cacheName: string): Promise<boolean> { | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 | ||||||
|  |     return delete this.data[cacheName] | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/cache/mod.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/cache/mod.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | export * from './adapter.ts' | ||||||
|  | export * from './default.ts' | ||||||
|  | // Not exported by default
 | ||||||
|  | // export * from './redis.ts'
 | ||||||
							
								
								
									
										286
									
								
								src/models/cacheAdapter.ts → src/cache/redis.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										286
									
								
								src/models/cacheAdapter.ts → src/cache/redis.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,176 +1,110 @@ | ||||||
| import { Collection } from '../utils/collection.ts' | import { ICacheAdapter } from './adapter.ts' | ||||||
| import { connect, Redis, RedisConnectOptions } from '../../deps.ts' | // Not in deps.ts to allow optional dep loading
 | ||||||
| 
 | import { | ||||||
| /** |   connect, | ||||||
|  * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. |   Redis, | ||||||
|  * |   RedisConnectOptions | ||||||
|  * Methods can return Promises too. | } from 'https://deno.land/x/redis@v0.14.1/mod.ts' | ||||||
|  */ | 
 | ||||||
| export interface ICacheAdapter { | /** Redis Cache Adapter for using Redis as a cache-provider. */ | ||||||
|   /** Gets a key from a Cache */ | export class RedisCacheAdapter implements ICacheAdapter { | ||||||
|   get: (cacheName: string, key: string) => Promise<any> | any |   _redis: Promise<Redis> | ||||||
|   /** Sets a key to value in a Cache Name with optional expire value in MS */ |   redis?: Redis | ||||||
|   set: ( |   ready: boolean = false | ||||||
|     cacheName: string, |   readonly _expireIntervalTimer: number = 5000 | ||||||
|     key: string, |   private _expireInterval?: number | ||||||
|     value: any, | 
 | ||||||
|     expire?: number |   constructor(options: RedisConnectOptions) { | ||||||
|   ) => Promise<any> | any |     this._redis = connect(options) | ||||||
|   /** Deletes a key from a Cache */ |     this._redis.then( | ||||||
|   delete: (cacheName: string, key: string) => Promise<boolean> | boolean |       (redis) => { | ||||||
|   /** Gets array of all values in a Cache */ |         this.redis = redis | ||||||
|   array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> |         this.ready = true | ||||||
|   /** Entirely deletes a Cache */ |         this._startExpireInterval() | ||||||
|   deleteCache: (cacheName: string) => any |       }, | ||||||
| } |       () => { | ||||||
| 
 |         // TODO: Make error for this
 | ||||||
| /** Default Cache Adapter for in-memory caching. */ |       } | ||||||
| export class DefaultCacheAdapter implements ICacheAdapter { |     ) | ||||||
|   data: { |   } | ||||||
|     [name: string]: Collection<string, any> | 
 | ||||||
|   } = {} |   private _startExpireInterval(): void { | ||||||
| 
 |     this._expireInterval = setInterval(() => { | ||||||
|   async get(cacheName: string, key: string): Promise<undefined | any> { |       this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { | ||||||
|     const cache = this.data[cacheName] |         for (const name of names) { | ||||||
|     if (cache === undefined) return |           this.redis?.hvals(name).then((vals) => { | ||||||
|     return cache.get(key) |             for (const val of vals) { | ||||||
|   } |               const expireVal: { | ||||||
| 
 |                 name: string | ||||||
|   async set( |                 key: string | ||||||
|     cacheName: string, |                 at: number | ||||||
|     key: string, |               } = JSON.parse(val) | ||||||
|     value: any, |               const expired = new Date().getTime() > expireVal.at | ||||||
|     expire?: number |               if (expired) this.redis?.hdel(expireVal.name, expireVal.key) | ||||||
|   ): Promise<any> { |             } | ||||||
|     let cache = this.data[cacheName] |           }) | ||||||
|     if (cache === undefined) { |         } | ||||||
|       this.data[cacheName] = new Collection() |       }) | ||||||
|       cache = this.data[cacheName] |     }, this._expireIntervalTimer) | ||||||
|     } |   } | ||||||
|     cache.set(key, value) | 
 | ||||||
|     if (expire !== undefined) |   async _checkReady(): Promise<void> { | ||||||
|       setTimeout(() => { |     if (!this.ready) await this._redis | ||||||
|         cache.delete(key) |   } | ||||||
|       }, expire) | 
 | ||||||
|   } |   async get(cacheName: string, key: string): Promise<string | undefined> { | ||||||
| 
 |     await this._checkReady() | ||||||
|   async delete(cacheName: string, key: string): Promise<boolean> { |     const cache = await this.redis?.hget(cacheName, key) | ||||||
|     const cache = this.data[cacheName] |     if (cache === undefined) return | ||||||
|     if (cache === undefined) return false |     try { | ||||||
|     return cache.delete(key) |       return JSON.parse(cache) | ||||||
|   } |     } catch (e) { | ||||||
| 
 |       return cache | ||||||
|   async array(cacheName: string): Promise<any[] | undefined> { |     } | ||||||
|     const cache = this.data[cacheName] |   } | ||||||
|     if (cache === undefined) return | 
 | ||||||
|     return cache.array() |   async set( | ||||||
|   } |     cacheName: string, | ||||||
| 
 |     key: string, | ||||||
|   async deleteCache(cacheName: string): Promise<boolean> { |     value: any, | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 |     expire?: number | ||||||
|     return delete this.data[cacheName] |   ): Promise<number | undefined> { | ||||||
|   } |     await this._checkReady() | ||||||
| } |     const result = await this.redis?.hset( | ||||||
| 
 |       cacheName, | ||||||
| /** Redis Cache Adapter for using Redis as a cache-provider. */ |       key, | ||||||
| export class RedisCacheAdapter implements ICacheAdapter { |       typeof value === 'object' ? JSON.stringify(value) : value | ||||||
|   _redis: Promise<Redis> |     ) | ||||||
|   redis?: Redis |     if (expire !== undefined) { | ||||||
|   ready: boolean = false |       await this.redis?.hset( | ||||||
|   readonly _expireIntervalTimer: number = 5000 |         `${cacheName}:expires`, | ||||||
|   private _expireInterval?: number |         key, | ||||||
| 
 |         JSON.stringify({ | ||||||
|   constructor(options: RedisConnectOptions) { |           name: cacheName, | ||||||
|     this._redis = connect(options) |           key, | ||||||
|     this._redis.then( |           at: new Date().getTime() + expire | ||||||
|       (redis) => { |         }) | ||||||
|         this.redis = redis |       ) | ||||||
|         this.ready = true |     } | ||||||
|         this._startExpireInterval() |     return result | ||||||
|       }, |   } | ||||||
|       () => { | 
 | ||||||
|         // TODO: Make error for this
 |   async delete(cacheName: string, key: string): Promise<boolean> { | ||||||
|       } |     await this._checkReady() | ||||||
|     ) |     const exists = await this.redis?.hexists(cacheName, key) | ||||||
|   } |     if (exists === 0) return false | ||||||
| 
 |     await this.redis?.hdel(cacheName, key) | ||||||
|   private _startExpireInterval(): void { |     return true | ||||||
|     this._expireInterval = setInterval(() => { |   } | ||||||
|       this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { | 
 | ||||||
|         for (const name of names) { |   async array(cacheName: string): Promise<any[] | undefined> { | ||||||
|           this.redis?.hvals(name).then((vals) => { |     await this._checkReady() | ||||||
|             for (const val of vals) { |     const data = await this.redis?.hvals(cacheName) | ||||||
|               const expireVal: { |     return data?.map((e: string) => JSON.parse(e)) | ||||||
|                 name: string |   } | ||||||
|                 key: string | 
 | ||||||
|                 at: number |   async deleteCache(cacheName: string): Promise<boolean> { | ||||||
|               } = JSON.parse(val) |     await this._checkReady() | ||||||
|               const expired = new Date().getTime() > expireVal.at |     return (await this.redis?.del(cacheName)) !== 0 | ||||||
|               if (expired) this.redis?.hdel(expireVal.name, expireVal.key) |   } | ||||||
|             } | } | ||||||
|           }) |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|     }, this._expireIntervalTimer) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async _checkReady(): Promise<void> { |  | ||||||
|     if (!this.ready) await this._redis |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async get(cacheName: string, key: string): Promise<string | undefined> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const cache = await this.redis?.hget(cacheName, key) |  | ||||||
|     if (cache === undefined) return |  | ||||||
|     try { |  | ||||||
|       return JSON.parse(cache) |  | ||||||
|     } catch (e) { |  | ||||||
|       return cache |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async set( |  | ||||||
|     cacheName: string, |  | ||||||
|     key: string, |  | ||||||
|     value: any, |  | ||||||
|     expire?: number |  | ||||||
|   ): Promise<number | undefined> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const result = await this.redis?.hset( |  | ||||||
|       cacheName, |  | ||||||
|       key, |  | ||||||
|       typeof value === 'object' ? JSON.stringify(value) : value |  | ||||||
|     ) |  | ||||||
|     if (expire !== undefined) { |  | ||||||
|       await this.redis?.hset( |  | ||||||
|         `${cacheName}:expires`, |  | ||||||
|         key, |  | ||||||
|         JSON.stringify({ |  | ||||||
|           name: cacheName, |  | ||||||
|           key, |  | ||||||
|           at: new Date().getTime() + expire |  | ||||||
|         }) |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|     return result |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async delete(cacheName: string, key: string): Promise<boolean> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const exists = await this.redis?.hexists(cacheName, key) |  | ||||||
|     if (exists === 0) return false |  | ||||||
|     await this.redis?.hdel(cacheName, key) |  | ||||||
|     return true |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async array(cacheName: string): Promise<any[] | undefined> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     const data = await this.redis?.hvals(cacheName) |  | ||||||
|     return data?.map((e: string) => JSON.parse(e)) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async deleteCache(cacheName: string): Promise<boolean> { |  | ||||||
|     await this._checkReady() |  | ||||||
|     return (await this.redis?.del(cacheName)) !== 0 |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,494 +1,440 @@ | ||||||
| /* eslint-disable @typescript-eslint/method-signature-style */ | /* eslint-disable @typescript-eslint/method-signature-style */ | ||||||
| import { User } from '../structures/user.ts' | import type { User } from '../structures/user.ts' | ||||||
| import { GatewayIntents } from '../types/gateway.ts' | import { GatewayIntents } from '../types/gateway.ts' | ||||||
| import { Gateway } from '../gateway/index.ts' | import { Gateway } from '../gateway/mod.ts' | ||||||
| import { RESTManager, RESTOptions, TokenType } from './rest.ts' | import { RESTManager, RESTOptions, TokenType } from '../rest/mod.ts' | ||||||
| import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' | import { DefaultCacheAdapter, ICacheAdapter } from '../cache/mod.ts' | ||||||
| import { UsersManager } from '../managers/users.ts' | import { UsersManager } from '../managers/users.ts' | ||||||
| import { GuildManager } from '../managers/guilds.ts' | import { GuildManager } from '../managers/guilds.ts' | ||||||
| import { ChannelsManager } from '../managers/channels.ts' | import { ChannelsManager } from '../managers/channels.ts' | ||||||
| import { ClientPresence } from '../structures/presence.ts' | import { ClientPresence } from '../structures/presence.ts' | ||||||
| import { EmojisManager } from '../managers/emojis.ts' | import { EmojisManager } from '../managers/emojis.ts' | ||||||
| import { ActivityGame, ClientActivity } from '../types/presence.ts' | import { ActivityGame, ClientActivity } from '../types/presence.ts' | ||||||
| import { Extension } from './extensions.ts' | import type { Extension } from '../commands/extension.ts' | ||||||
| import { SlashClient } from './slashClient.ts' | import { SlashClient } from '../interactions/slashClient.ts' | ||||||
| import { Interaction } from '../structures/slash.ts' | import type { Interaction } from '../structures/slash.ts' | ||||||
| import { SlashModule } from './slashModule.ts' | import { ShardManager } from './shard.ts' | ||||||
| import { ShardManager } from './shard.ts' | import { Application } from '../structures/application.ts' | ||||||
| import { Application } from '../structures/application.ts' | import { Invite } from '../structures/invite.ts' | ||||||
| import { Invite } from '../structures/invite.ts' | import { INVITE } from '../types/endpoint.ts' | ||||||
| import { INVITE } from '../types/endpoint.ts' | import type { ClientEvents } from '../gateway/handlers/mod.ts' | ||||||
| import { ClientEvents } from '../gateway/handlers/index.ts' | import type { Collector } from './collectors.ts' | ||||||
| import type { Collector } from './collectors.ts' | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | import type { VoiceRegion } from '../types/voice.ts' | ||||||
| import { VoiceRegion } from '../types/voice.ts' | import { fetchAuto } from '../../deps.ts' | ||||||
| import { fetchAuto } from '../../deps.ts' | import type { DMChannel } from '../structures/dmChannel.ts' | ||||||
| import { DMChannel } from '../structures/dmChannel.ts' | import { Template } from '../structures/template.ts' | ||||||
| import { Template } from '../structures/template.ts' | 
 | ||||||
| 
 | /** OS related properties sent with Gateway Identify */ | ||||||
| /** OS related properties sent with Gateway Identify */ | export interface ClientProperties { | ||||||
| export interface ClientProperties { |   os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string | ||||||
|   os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string |   browser?: 'harmony' | string | ||||||
|   browser?: 'harmony' | string |   device?: 'harmony' | string | ||||||
|   device?: 'harmony' | string | } | ||||||
| } | 
 | ||||||
| 
 | /** Some Client Options to modify behaviour */ | ||||||
| /** Some Client Options to modify behaviour */ | export interface ClientOptions { | ||||||
| export interface ClientOptions { |   /** ID of the Client/Application to initialize Slash Client REST */ | ||||||
|   /** ID of the Client/Application to initialize Slash Client REST */ |   id?: string | ||||||
|   id?: string |   /** Token of the Bot/User */ | ||||||
|   /** Token of the Bot/User */ |   token?: string | ||||||
|   token?: string |   /** Gateway Intents */ | ||||||
|   /** Gateway Intents */ |   intents?: GatewayIntents[] | ||||||
|   intents?: GatewayIntents[] |   /** Cache Adapter to use, defaults to Collections one */ | ||||||
|   /** Cache Adapter to use, defaults to Collections one */ |   cache?: ICacheAdapter | ||||||
|   cache?: ICacheAdapter |   /** Force New Session and don't use cached Session (by persistent caching) */ | ||||||
|   /** Force New Session and don't use cached Session (by persistent caching) */ |   forceNewSession?: boolean | ||||||
|   forceNewSession?: boolean |   /** Startup presence of client */ | ||||||
|   /** Startup presence of client */ |   presence?: ClientPresence | ClientActivity | ActivityGame | ||||||
|   presence?: ClientPresence | ClientActivity | ActivityGame |   /** Force all requests to Canary API */ | ||||||
|   /** Force all requests to Canary API */ |   canary?: boolean | ||||||
|   canary?: boolean |   /** Time till which Messages are to be cached, in MS. Default is 3600000 */ | ||||||
|   /** Time till which Messages are to be cached, in MS. Default is 3600000 */ |   messageCacheLifetime?: number | ||||||
|   messageCacheLifetime?: number |   /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ | ||||||
|   /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ |   reactionCacheLifetime?: number | ||||||
|   reactionCacheLifetime?: number |   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ |   fetchUncachedReactions?: boolean | ||||||
|   fetchUncachedReactions?: boolean |   /** Client Properties */ | ||||||
|   /** Client Properties */ |   clientProperties?: ClientProperties | ||||||
|   clientProperties?: ClientProperties |   /** Enable/Disable Slash Commands Integration (enabled by default) */ | ||||||
|   /** Enable/Disable Slash Commands Integration (enabled by default) */ |   enableSlash?: boolean | ||||||
|   enableSlash?: boolean |   /** Disable taking token from env if not provided (token is taken from env if present by default) */ | ||||||
|   /** Disable taking token from env if not provided (token is taken from env if present by default) */ |   disableEnvToken?: boolean | ||||||
|   disableEnvToken?: boolean |   /** Override REST Options */ | ||||||
|   /** Override REST Options */ |   restOptions?: RESTOptions | ||||||
|   restOptions?: RESTOptions |   /** Whether to fetch Gateway info or not */ | ||||||
|   /** Whether to fetch Gateway info or not */ |   fetchGatewayInfo?: boolean | ||||||
|   fetchGatewayInfo?: boolean |   /** ADVANCED: Shard ID to launch on */ | ||||||
|   /** ADVANCED: Shard ID to launch on */ |   shard?: number | ||||||
|   shard?: number |   /** ADVACNED: Shard count. */ | ||||||
|   /** ADVACNED: Shard count. */ |   shardCount?: number | 'auto' | ||||||
|   shardCount?: number | 'auto' | } | ||||||
| } | 
 | ||||||
| 
 | /** | ||||||
| /** |  * Harmony Client. Provides high-level interface over the REST and WebSocket API. | ||||||
|  * Discord Client. |  */ | ||||||
|  */ | export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
| export class Client extends HarmonyEventEmitter<ClientEvents> { |   /** REST Manager - used to make all requests */ | ||||||
|   /** REST Manager - used to make all requests */ |   rest: RESTManager | ||||||
|   rest: RESTManager |   /** User which Client logs in to, undefined until logs in */ | ||||||
|   /** User which Client logs in to, undefined until logs in */ |   user?: User | ||||||
|   user?: User |   /** Token of the Bot/User */ | ||||||
|   /** WebSocket ping of Client */ |   token?: string | ||||||
|   ping = 0 |   /** Cache Adapter */ | ||||||
|   /** Token of the Bot/User */ |   cache: ICacheAdapter = new DefaultCacheAdapter() | ||||||
|   token?: string |   /** Gateway Intents */ | ||||||
|   /** Cache Adapter */ |   intents?: GatewayIntents[] | ||||||
|   cache: ICacheAdapter = new DefaultCacheAdapter() |   /** Whether to force new session or not */ | ||||||
|   /** Gateway Intents */ |   forceNewSession?: boolean | ||||||
|   intents?: GatewayIntents[] |   /** Time till messages to stay cached, in MS. */ | ||||||
|   /** Whether to force new session or not */ |   messageCacheLifetime: number = 3600000 | ||||||
|   forceNewSession?: boolean |   /** Time till messages to stay cached, in MS. */ | ||||||
|   /** Time till messages to stay cached, in MS. */ |   reactionCacheLifetime: number = 3600000 | ||||||
|   messageCacheLifetime: number = 3600000 |   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||||
|   /** Time till messages to stay cached, in MS. */ |   fetchUncachedReactions: boolean = false | ||||||
|   reactionCacheLifetime: number = 3600000 |   /** Client Properties */ | ||||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ |   clientProperties: ClientProperties | ||||||
|   fetchUncachedReactions: boolean = false |   /** Slash-Commands Management client */ | ||||||
|   /** Client Properties */ |   slash: SlashClient | ||||||
|   clientProperties: ClientProperties |   /** Whether to fetch Gateway info or not */ | ||||||
|   /** Slash-Commands Management client */ |   fetchGatewayInfo: boolean = true | ||||||
|   slash: SlashClient | 
 | ||||||
|   /** Whether to fetch Gateway info or not */ |   /** Users Manager, containing all Users cached */ | ||||||
|   fetchGatewayInfo: boolean = true |   users: UsersManager = new UsersManager(this) | ||||||
| 
 |   /** Guilds Manager, providing cache & API interface to Guilds */ | ||||||
|   /** Users Manager, containing all Users cached */ |   guilds: GuildManager = new GuildManager(this) | ||||||
|   users: UsersManager = new UsersManager(this) |   /** Channels Manager, providing cache interface to Channels */ | ||||||
|   /** Guilds Manager, providing cache & API interface to Guilds */ |   channels: ChannelsManager = new ChannelsManager(this) | ||||||
|   guilds: GuildManager = new GuildManager(this) |   /** Channels Manager, providing cache interface to Channels */ | ||||||
|   /** Channels Manager, providing cache interface to Channels */ |   emojis: EmojisManager = new EmojisManager(this) | ||||||
|   channels: ChannelsManager = new ChannelsManager(this) | 
 | ||||||
|   /** Channels Manager, providing cache interface to Channels */ |   /** Last READY timestamp */ | ||||||
|   emojis: EmojisManager = new EmojisManager(this) |   upSince?: Date | ||||||
| 
 | 
 | ||||||
|   /** Last READY timestamp */ |   /** Client's presence. Startup one if set before connecting */ | ||||||
|   upSince?: Date |   presence: ClientPresence = new ClientPresence() | ||||||
| 
 |   _decoratedEvents?: { | ||||||
|   /** Client's presence. Startup one if set before connecting */ |     [name: string]: (...args: any[]) => void | ||||||
|   presence: ClientPresence = new ClientPresence() |   } | ||||||
|   _decoratedEvents?: { | 
 | ||||||
|     [name: string]: (...args: any[]) => void |   _decoratedSlash?: Array<{ | ||||||
|   } |     name: string | ||||||
| 
 |     guild?: string | ||||||
|   _decoratedSlash?: Array<{ |     parent?: string | ||||||
|     name: string |     group?: string | ||||||
|     guild?: string |     handler: (interaction: Interaction) => any | ||||||
|     parent?: string |   }> | ||||||
|     group?: string | 
 | ||||||
|     handler: (interaction: Interaction) => any |   _id?: string | ||||||
|   }> | 
 | ||||||
| 
 |   /** Shard on which this Client is */ | ||||||
|   _id?: string |   shard?: number | ||||||
| 
 |   /** Shard Count */ | ||||||
|   /** Shard on which this Client is */ |   shardCount: number | 'auto' = 'auto' | ||||||
|   shard?: number |   /** Shard Manager of this Client if Sharded */ | ||||||
|   /** Shard Count */ |   shards: ShardManager | ||||||
|   shardCount: number | 'auto' = 'auto' |   /** Collectors set */ | ||||||
|   /** Shard Manager of this Client if Sharded */ |   collectors: Set<Collector> = new Set() | ||||||
|   shards: ShardManager | 
 | ||||||
|   /** Collectors set */ |   /** Since when is Client online (ready). */ | ||||||
|   collectors: Set<Collector> = new Set() |   get uptime(): number { | ||||||
| 
 |     if (this.upSince === undefined) return 0 | ||||||
|   /** Since when is Client online (ready). */ |     else { | ||||||
|   get uptime(): number { |       const dif = Date.now() - this.upSince.getTime() | ||||||
|     if (this.upSince === undefined) return 0 |       if (dif < 0) return 0 | ||||||
|     else { |       else return dif | ||||||
|       const dif = Date.now() - this.upSince.getTime() |     } | ||||||
|       if (dif < 0) return 0 |   } | ||||||
|       else return dif | 
 | ||||||
|     } |   /** Get Shard 0's Gateway */ | ||||||
|   } |   get gateway(): Gateway { | ||||||
| 
 |     return this.shards.list.get('0')! | ||||||
|   get gateway(): Gateway { |   } | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
 | 
 | ||||||
|     return this.shards.list.get('0') as Gateway |   applicationID?: string | ||||||
|   } |   applicationFlags?: number | ||||||
| 
 | 
 | ||||||
|   applicationID?: string |   constructor(options: ClientOptions = {}) { | ||||||
|   applicationFlags?: number |     super() | ||||||
| 
 |     this._id = options.id | ||||||
|   constructor(options: ClientOptions = {}) { |     this.token = options.token | ||||||
|     super() |     this.intents = options.intents | ||||||
|     this._id = options.id |     this.shards = new ShardManager(this) | ||||||
|     this.token = options.token |     this.forceNewSession = options.forceNewSession | ||||||
|     this.intents = options.intents |     if (options.cache !== undefined) this.cache = options.cache | ||||||
|     this.shards = new ShardManager(this) |     if (options.presence !== undefined) | ||||||
|     this.forceNewSession = options.forceNewSession |       this.presence = | ||||||
|     if (options.cache !== undefined) this.cache = options.cache |         options.presence instanceof ClientPresence | ||||||
|     if (options.presence !== undefined) |           ? options.presence | ||||||
|       this.presence = |           : new ClientPresence(options.presence) | ||||||
|         options.presence instanceof ClientPresence |     if (options.messageCacheLifetime !== undefined) | ||||||
|           ? options.presence |       this.messageCacheLifetime = options.messageCacheLifetime | ||||||
|           : new ClientPresence(options.presence) |     if (options.reactionCacheLifetime !== undefined) | ||||||
|     if (options.messageCacheLifetime !== undefined) |       this.reactionCacheLifetime = options.reactionCacheLifetime | ||||||
|       this.messageCacheLifetime = options.messageCacheLifetime |     if (options.fetchUncachedReactions === true) | ||||||
|     if (options.reactionCacheLifetime !== undefined) |       this.fetchUncachedReactions = true | ||||||
|       this.reactionCacheLifetime = options.reactionCacheLifetime | 
 | ||||||
|     if (options.fetchUncachedReactions === true) |     if ( | ||||||
|       this.fetchUncachedReactions = true |       this._decoratedEvents !== undefined && | ||||||
| 
 |       Object.keys(this._decoratedEvents).length !== 0 | ||||||
|     if ( |     ) { | ||||||
|       this._decoratedEvents !== undefined && |       Object.entries(this._decoratedEvents).forEach((entry) => { | ||||||
|       Object.keys(this._decoratedEvents).length !== 0 |         this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) | ||||||
|     ) { |       }) | ||||||
|       Object.entries(this._decoratedEvents).forEach((entry) => { |       this._decoratedEvents = undefined | ||||||
|         this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) |     } | ||||||
|       }) | 
 | ||||||
|       this._decoratedEvents = undefined |     this.clientProperties = | ||||||
|     } |       options.clientProperties === undefined | ||||||
| 
 |         ? { | ||||||
|     this.clientProperties = |             os: Deno.build.os, | ||||||
|       options.clientProperties === undefined |             browser: 'harmony', | ||||||
|         ? { |             device: 'harmony' | ||||||
|             os: Deno.build.os, |           } | ||||||
|             browser: 'harmony', |         : options.clientProperties | ||||||
|             device: 'harmony' | 
 | ||||||
|           } |     if (options.shard !== undefined) this.shard = options.shard | ||||||
|         : options.clientProperties |     if (options.shardCount !== undefined) this.shardCount = options.shardCount | ||||||
| 
 | 
 | ||||||
|     if (options.shard !== undefined) this.shard = options.shard |     this.fetchGatewayInfo = options.fetchGatewayInfo ?? true | ||||||
|     if (options.shardCount !== undefined) this.shardCount = options.shardCount | 
 | ||||||
| 
 |     if (this.token === undefined) { | ||||||
|     this.fetchGatewayInfo = options.fetchGatewayInfo ?? true |       try { | ||||||
| 
 |         const token = Deno.env.get('DISCORD_TOKEN') | ||||||
|     if (this.token === undefined) { |         if (token !== undefined) { | ||||||
|       try { |           this.token = token | ||||||
|         const token = Deno.env.get('DISCORD_TOKEN') |           this.debug('Info', 'Found token in ENV') | ||||||
|         if (token !== undefined) { |         } | ||||||
|           this.token = token |       } catch (e) {} | ||||||
|           this.debug('Info', 'Found token in ENV') |     } | ||||||
|         } | 
 | ||||||
|       } catch (e) {} |     const restOptions: RESTOptions = { | ||||||
|     } |       token: () => this.token, | ||||||
| 
 |       tokenType: TokenType.Bot, | ||||||
|     const restOptions: RESTOptions = { |       canary: options.canary, | ||||||
|       token: () => this.token, |       client: this | ||||||
|       tokenType: TokenType.Bot, |     } | ||||||
|       canary: options.canary, | 
 | ||||||
|       client: this |     if (options.restOptions !== undefined) | ||||||
|     } |       Object.assign(restOptions, options.restOptions) | ||||||
| 
 |     this.rest = new RESTManager(restOptions) | ||||||
|     if (options.restOptions !== undefined) | 
 | ||||||
|       Object.assign(restOptions, options.restOptions) |     this.slash = new SlashClient({ | ||||||
|     this.rest = new RESTManager(restOptions) |       id: () => this.getEstimatedID(), | ||||||
| 
 |       client: this, | ||||||
|     this.slash = new SlashClient({ |       enabled: options.enableSlash | ||||||
|       id: () => this.getEstimatedID(), |     }) | ||||||
|       client: this, |   } | ||||||
|       enabled: options.enableSlash | 
 | ||||||
|     }) |   /** | ||||||
|   } |    * Sets Cache Adapter | ||||||
| 
 |    * | ||||||
|   /** |    * Should NOT be set after bot is already logged in or using current cache. | ||||||
|    * Sets Cache Adapter |    * Please look into using `cache` option. | ||||||
|    * |    */ | ||||||
|    * Should NOT be set after bot is already logged in or using current cache. |   setAdapter(adapter: ICacheAdapter): Client { | ||||||
|    * Please look into using `cache` option. |     this.cache = adapter | ||||||
|    */ |     return this | ||||||
|   setAdapter(adapter: ICacheAdapter): Client { |   } | ||||||
|     this.cache = adapter | 
 | ||||||
|     return this |   /** Changes Presence of Client */ | ||||||
|   } |   setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { | ||||||
| 
 |     if (presence instanceof ClientPresence) { | ||||||
|   /** Changes Presence of Client */ |       this.presence = presence | ||||||
|   setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { |     } else this.presence = new ClientPresence(presence) | ||||||
|     if (presence instanceof ClientPresence) { |     this.gateway?.sendPresence(this.presence.create()) | ||||||
|       this.presence = presence |   } | ||||||
|     } else this.presence = new ClientPresence(presence) | 
 | ||||||
|     this.gateway?.sendPresence(this.presence.create()) |   /** Emits debug event */ | ||||||
|   } |   debug(tag: string, msg: string): void { | ||||||
| 
 |     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||||
|   /** Emits debug event */ |     this.emit('debug', `[${tag}] ${msg}`) | ||||||
|   debug(tag: string, msg: string): void { |   } | ||||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
 | ||||||
|     this.emit('debug', `[${tag}] ${msg}`) |   getEstimatedID(): string { | ||||||
|   } |     if (this.user !== undefined) return this.user.id | ||||||
| 
 |     else if (this.token !== undefined) { | ||||||
|   getEstimatedID(): string { |       try { | ||||||
|     if (this.user !== undefined) return this.user.id |         return atob(this.token.split('.')[0]) | ||||||
|     else if (this.token !== undefined) { |       } catch (e) { | ||||||
|       try { |         return this._id ?? 'unknown' | ||||||
|         return atob(this.token.split('.')[0]) |       } | ||||||
|       } catch (e) { |     } else { | ||||||
|         return this._id ?? 'unknown' |       return this._id ?? 'unknown' | ||||||
|       } |     } | ||||||
|     } else { |   } | ||||||
|       return this._id ?? 'unknown' | 
 | ||||||
|     } |   /** Fetch Application of the Client */ | ||||||
|   } |   async fetchApplication(): Promise<Application> { | ||||||
| 
 |     const app = await this.rest.api.oauth2.applications['@me'].get() | ||||||
|   /** Fetch Application of the Client */ |     return new Application(this, app) | ||||||
|   async fetchApplication(): Promise<Application> { |   } | ||||||
|     const app = await this.rest.api.oauth2.applications['@me'].get() | 
 | ||||||
|     return new Application(this, app) |   /** Fetch an Invite */ | ||||||
|   } |   async fetchInvite(id: string): Promise<Invite> { | ||||||
| 
 |     return await new Promise((resolve, reject) => { | ||||||
|   /** Fetch an Invite */ |       this.rest | ||||||
|   async fetchInvite(id: string): Promise<Invite> { |         .get(INVITE(id)) | ||||||
|     return await new Promise((resolve, reject) => { |         .then((data) => { | ||||||
|       this.rest |           resolve(new Invite(this, data)) | ||||||
|         .get(INVITE(id)) |         }) | ||||||
|         .then((data) => { |         .catch((e) => reject(e)) | ||||||
|           resolve(new Invite(this, data)) |     }) | ||||||
|         }) |   } | ||||||
|         .catch((e) => reject(e)) | 
 | ||||||
|     }) |   /** | ||||||
|   } |    * This function is used for connecting to discord. | ||||||
| 
 |    * @param token Your token. This is required if not given in ClientOptions. | ||||||
|   /** |    * @param intents Gateway intents in array. This is required if not given in ClientOptions. | ||||||
|    * This function is used for connecting to discord. |    */ | ||||||
|    * @param token Your token. This is required if not given in ClientOptions. |   async connect( | ||||||
|    * @param intents Gateway intents in array. This is required if not given in ClientOptions. |     token?: string, | ||||||
|    */ |     intents?: Array<GatewayIntents | keyof typeof GatewayIntents> | ||||||
|   async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> { |   ): Promise<Client> { | ||||||
|     token ??= this.token |     token ??= this.token | ||||||
|     if (token === undefined) throw new Error('No Token Provided') |     if (token === undefined) throw new Error('No Token Provided') | ||||||
|     this.token = token |     this.token = token | ||||||
|     if (intents !== undefined && this.intents !== undefined) { |     if (intents !== undefined && this.intents !== undefined) { | ||||||
|       this.debug( |       this.debug( | ||||||
|         'client', |         'client', | ||||||
|         'Intents were set in both client and connect function. Using the one in the connect function...' |         'Intents were set in both client and connect function. Using the one in the connect function...' | ||||||
|       ) |       ) | ||||||
|     } else if (intents === undefined && this.intents !== undefined) { |     } else if (intents === undefined && this.intents !== undefined) { | ||||||
|       intents = this.intents |       intents = this.intents | ||||||
|     } else if (intents !== undefined && this.intents === undefined) { |     } else if (intents !== undefined && this.intents === undefined) { | ||||||
|       this.intents = intents |       this.intents = intents.map((e) => | ||||||
|     } else throw new Error('No Gateway Intents were provided') |         typeof e === 'string' ? GatewayIntents[e] : e | ||||||
| 
 |       ) | ||||||
|     this.rest.token = token |     } else throw new Error('No Gateway Intents were provided') | ||||||
|     if (this.shard !== undefined) { | 
 | ||||||
|       if (typeof this.shardCount === 'number') |     this.rest.token = token | ||||||
|         this.shards.cachedShardCount = this.shardCount |     if (this.shard !== undefined) { | ||||||
|       await this.shards.launch(this.shard) |       if (typeof this.shardCount === 'number') | ||||||
|     } else await this.shards.connect() |         this.shards.cachedShardCount = this.shardCount | ||||||
|     return this.waitFor('ready', () => true).then(() => this) |       await this.shards.launch(this.shard) | ||||||
|   } |     } else await this.shards.connect() | ||||||
| 
 |     return this.waitFor('ready', () => true).then(() => this) | ||||||
|   /** Destroy the Gateway connection */ |   } | ||||||
|   async destroy(): Promise<Client> { | 
 | ||||||
|     this.gateway.initialized = false |   /** Destroy the Gateway connection */ | ||||||
|     this.gateway.sequenceID = undefined |   async destroy(): Promise<Client> { | ||||||
|     this.gateway.sessionID = undefined |     this.gateway.initialized = false | ||||||
|     await this.gateway.cache.delete('seq') |     this.gateway.sequenceID = undefined | ||||||
|     await this.gateway.cache.delete('session_id') |     this.gateway.sessionID = undefined | ||||||
|     this.gateway.close() |     await this.gateway.cache.delete('seq') | ||||||
|     this.user = undefined |     await this.gateway.cache.delete('session_id') | ||||||
|     this.upSince = undefined |     this.gateway.close() | ||||||
|     return this |     this.user = undefined | ||||||
|   } |     this.upSince = undefined | ||||||
| 
 |     return this | ||||||
|   /** Attempt to Close current Gateway connection and Resume */ |   } | ||||||
|   async reconnect(): Promise<Client> { | 
 | ||||||
|     this.gateway.close() |   /** Attempt to Close current Gateway connection and Resume */ | ||||||
|     this.gateway.initWebsocket() |   async reconnect(): Promise<Client> { | ||||||
|     return this.waitFor('ready', () => true).then(() => this) |     this.gateway.close() | ||||||
|   } |     this.gateway.initWebsocket() | ||||||
| 
 |     return this.waitFor('ready', () => true).then(() => this) | ||||||
|   /** Add a new Collector */ |   } | ||||||
|   addCollector(collector: Collector): boolean { | 
 | ||||||
|     if (this.collectors.has(collector)) return false |   /** Add a new Collector */ | ||||||
|     else { |   addCollector(collector: Collector): boolean { | ||||||
|       this.collectors.add(collector) |     if (this.collectors.has(collector)) return false | ||||||
|       return true |     else { | ||||||
|     } |       this.collectors.add(collector) | ||||||
|   } |       return true | ||||||
| 
 |     } | ||||||
|   /** Remove a Collector */ |   } | ||||||
|   removeCollector(collector: Collector): boolean { | 
 | ||||||
|     if (!this.collectors.has(collector)) return false |   /** Remove a Collector */ | ||||||
|     else { |   removeCollector(collector: Collector): boolean { | ||||||
|       this.collectors.delete(collector) |     if (!this.collectors.has(collector)) return false | ||||||
|       return true |     else { | ||||||
|     } |       this.collectors.delete(collector) | ||||||
|   } |       return true | ||||||
| 
 |     } | ||||||
|   async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> { |   } | ||||||
|     const collectors: Collector[] = [] | 
 | ||||||
|     for (const collector of this.collectors.values()) { |   async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> { | ||||||
|       if (collector.event === event) collectors.push(collector) |     const collectors: Collector[] = [] | ||||||
|     } |     for (const collector of this.collectors.values()) { | ||||||
|     if (collectors.length !== 0) { |       if (collector.event === event) collectors.push(collector) | ||||||
|       this.collectors.forEach((collector) => collector._fire(...args)) |     } | ||||||
|     } |     if (collectors.length !== 0) { | ||||||
|     // TODO(DjDeveloperr): Fix this ts-ignore
 |       this.collectors.forEach((collector) => collector._fire(...args)) | ||||||
|     // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
 |     } | ||||||
|     // @ts-ignore
 |     // TODO(DjDeveloperr): Fix this ts-ignore
 | ||||||
|     return super.emit(event, ...args) |     // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
 | ||||||
|   } |     // @ts-ignore
 | ||||||
| 
 |     return super.emit(event, ...args) | ||||||
|   /** Returns an array of voice region objects that can be used when creating servers. */ |   } | ||||||
|   async fetchVoiceRegions(): Promise<VoiceRegion[]> { | 
 | ||||||
|     return this.rest.api.voice.regions.get() |   /** Returns an array of voice region objects that can be used when creating servers. */ | ||||||
|   } |   async fetchVoiceRegions(): Promise<VoiceRegion[]> { | ||||||
| 
 |     return this.rest.api.voice.regions.get() | ||||||
|   /** Modify current (Client) User. */ |   } | ||||||
|   async editUser(data: { | 
 | ||||||
|     username?: string |   /** Modify current (Client) User. */ | ||||||
|     avatar?: string |   async editUser(data: { | ||||||
|   }): Promise<Client> { |     username?: string | ||||||
|     if (data.username === undefined && data.avatar === undefined) |     avatar?: string | ||||||
|       throw new Error( |   }): Promise<Client> { | ||||||
|         'Either username or avatar or both must be specified to edit' |     if (data.username === undefined && data.avatar === undefined) | ||||||
|       ) |       throw new Error( | ||||||
| 
 |         'Either username or avatar or both must be specified to edit' | ||||||
|     if (data.avatar?.startsWith('http') === true) { |       ) | ||||||
|       data.avatar = await fetchAuto(data.avatar) | 
 | ||||||
|     } |     if (data.avatar?.startsWith('http') === true) { | ||||||
| 
 |       data.avatar = await fetchAuto(data.avatar) | ||||||
|     await this.rest.api.users['@me'].patch({ |     } | ||||||
|       username: data.username, | 
 | ||||||
|       avatar: data.avatar |     await this.rest.api.users['@me'].patch({ | ||||||
|     }) |       username: data.username, | ||||||
|     return this |       avatar: data.avatar | ||||||
|   } |     }) | ||||||
| 
 |     return this | ||||||
|   /** Change Username of the Client User */ |   } | ||||||
|   async setUsername(username: string): Promise<Client> { | 
 | ||||||
|     return await this.editUser({ username }) |   /** Change Username of the Client User */ | ||||||
|   } |   async setUsername(username: string): Promise<Client> { | ||||||
| 
 |     return await this.editUser({ username }) | ||||||
|   /** Change Avatar of the Client User */ |   } | ||||||
|   async setAvatar(avatar: string): Promise<Client> { | 
 | ||||||
|     return await this.editUser({ avatar }) |   /** Change Avatar of the Client User */ | ||||||
|   } |   async setAvatar(avatar: string): Promise<Client> { | ||||||
| 
 |     return await this.editUser({ avatar }) | ||||||
|   /** Create a DM Channel with a User */ |   } | ||||||
|   async createDM(user: User | string): Promise<DMChannel> { | 
 | ||||||
|     const id = typeof user === 'object' ? user.id : user |   /** Create a DM Channel with a User */ | ||||||
|     const dmPayload = await this.rest.api.users['@me'].channels.post({ |   async createDM(user: User | string): Promise<DMChannel> { | ||||||
|       recipient_id: id |     const id = typeof user === 'object' ? user.id : user | ||||||
|     }) |     const dmPayload = await this.rest.api.users['@me'].channels.post({ | ||||||
|     await this.channels.set(dmPayload.id, dmPayload) |       recipient_id: id | ||||||
|     return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel |     }) | ||||||
|   } |     await this.channels.set(dmPayload.id, dmPayload) | ||||||
| 
 |     return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel | ||||||
|   /** Returns a template object for the given code. */ |   } | ||||||
|   async fetchTemplate(code: string): Promise<Template> { | 
 | ||||||
|     const payload = await this.rest.api.guilds.templates[code].get() |   /** Returns a template object for the given code. */ | ||||||
|     return new Template(this, payload) |   async fetchTemplate(code: string): Promise<Template> { | ||||||
|   } |     const payload = await this.rest.api.guilds.templates[code].get() | ||||||
| } |     return new Template(this, payload) | ||||||
| 
 |   } | ||||||
| /** Event decorator to create an Event handler from function */ | } | ||||||
| export function event(name?: keyof ClientEvents) { | 
 | ||||||
|   return function ( | /** Event decorator to create an Event handler from function */ | ||||||
|     client: Client | Extension, | export function event(name?: keyof ClientEvents) { | ||||||
|     prop: keyof ClientEvents | string |   return function ( | ||||||
|   ) { |     client: Client | Extension, | ||||||
|     const listener = ((client as unknown) as { |     prop: keyof ClientEvents | string | ||||||
|       [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any |   ) { | ||||||
|     })[(prop as unknown) as keyof ClientEvents] |     const listener = ((client as unknown) as { | ||||||
|     if (typeof listener !== 'function') |       [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any | ||||||
|       throw new Error('@event decorator requires a function') |     })[(prop as unknown) as keyof ClientEvents] | ||||||
|     if (client._decoratedEvents === undefined) client._decoratedEvents = {} |     if (typeof listener !== 'function') | ||||||
|     const key = name === undefined ? prop : name |       throw new Error('@event decorator requires a function') | ||||||
| 
 |     if (client._decoratedEvents === undefined) client._decoratedEvents = {} | ||||||
|     client._decoratedEvents[key] = listener |     const key = name === undefined ? prop : name | ||||||
|   } | 
 | ||||||
| } |     client._decoratedEvents[key] = listener | ||||||
| 
 |   } | ||||||
| /** Decorator to create a Slash Command handler */ | } | ||||||
| export function slash(name?: string, guild?: string) { |  | ||||||
|   return function (client: Client | SlashClient | SlashModule, prop: string) { |  | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |  | ||||||
|     const item = (client as { [name: string]: any })[prop] |  | ||||||
|     if (typeof item !== 'function') { |  | ||||||
|       throw new Error('@slash decorator requires a function') |  | ||||||
|     } else |  | ||||||
|       client._decoratedSlash.push({ |  | ||||||
|         name: name ?? prop, |  | ||||||
|         guild, |  | ||||||
|         handler: item |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** Decorator to create a Sub-Slash Command handler */ |  | ||||||
| export function subslash(parent: string, name?: string, guild?: 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') { |  | ||||||
|       throw new Error('@subslash decorator requires a function') |  | ||||||
|     } else |  | ||||||
|       client._decoratedSlash.push({ |  | ||||||
|         parent, |  | ||||||
|         name: name ?? prop, |  | ||||||
|         guild, |  | ||||||
|         handler: item |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** Decorator to create a Grouped Slash Command handler */ |  | ||||||
| export function groupslash( |  | ||||||
|   parent: string, |  | ||||||
|   group: string, |  | ||||||
|   name?: string, |  | ||||||
|   guild?: 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') { |  | ||||||
|       throw new Error('@groupslash decorator requires a function') |  | ||||||
|     } else |  | ||||||
|       client._decoratedSlash.push({ |  | ||||||
|         group, |  | ||||||
|         parent, |  | ||||||
|         name: name ?? prop, |  | ||||||
|         guild, |  | ||||||
|         handler: item |  | ||||||
|       }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import type { Client } from './client.ts' | import type { Client } from '../client/client.ts' | ||||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
| 
 | 
 | ||||||
| export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean> | export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean> | ||||||
							
								
								
									
										3
									
								
								src/client/mod.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/client/mod.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export * from './client.ts' | ||||||
|  | export * from './collectors.ts' | ||||||
|  | export * from './shard.ts' | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import type { Client } from './client.ts' | import type { Client } from './client.ts' | ||||||
| import { RESTManager } from './rest.ts' | import { RESTManager } from '../rest/mod.ts' | ||||||
| import { Gateway } from '../gateway/index.ts' | import { Gateway } from '../gateway/mod.ts' | ||||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
| import { GatewayEvents } from '../types/gateway.ts' | import { GatewayEvents } from '../types/gateway.ts' | ||||||
| import { delay } from '../utils/delay.ts' | import { delay } from '../utils/delay.ts' | ||||||
|  | @ -61,10 +61,24 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> { | ||||||
|     let shardCount: number |     let shardCount: number | ||||||
|     if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount |     if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount | ||||||
|     else { |     else { | ||||||
|       if (this.client.shardCount === 'auto') { |       if ( | ||||||
|  |         this.client.shardCount === 'auto' && | ||||||
|  |         this.client.fetchGatewayInfo !== false | ||||||
|  |       ) { | ||||||
|  |         this.debug('Fetch /gateway/bot...') | ||||||
|         const info = await this.client.rest.api.gateway.bot.get() |         const info = await this.client.rest.api.gateway.bot.get() | ||||||
|  |         this.debug(`Recommended Shards: ${info.shards}`) | ||||||
|  |         this.debug('=== Session Limit Info ===') | ||||||
|  |         this.debug( | ||||||
|  |           `Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}` | ||||||
|  |         ) | ||||||
|  |         this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) | ||||||
|         shardCount = info.shards as number |         shardCount = info.shards as number | ||||||
|       } else shardCount = this.client.shardCount ?? 1 |       } else | ||||||
|  |         shardCount = | ||||||
|  |           typeof this.client.shardCount === 'string' | ||||||
|  |             ? 1 | ||||||
|  |             : this.client.shardCount ?? 1 | ||||||
|     } |     } | ||||||
|     this.cachedShardCount = shardCount |     this.cachedShardCount = shardCount | ||||||
|     return this.cachedShardCount |     return this.cachedShardCount | ||||||
|  | @ -79,8 +93,6 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> { | ||||||
|     const shardCount = await this.getShardCount() |     const shardCount = await this.getShardCount() | ||||||
| 
 | 
 | ||||||
|     const gw = new Gateway(this.client, [Number(id), shardCount]) |     const gw = new Gateway(this.client, [Number(id), shardCount]) | ||||||
|     gw.token = this.client.token |  | ||||||
|     gw.intents = this.client.intents |  | ||||||
|     this.list.set(id.toString(), gw) |     this.list.set(id.toString(), gw) | ||||||
| 
 | 
 | ||||||
|     gw.initWebsocket() |     gw.initWebsocket() | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Message } from '../structures/message.ts' | import type { Message } from '../structures/message.ts' | ||||||
| import { GuildTextBasedChannel } from '../structures/guildTextChannel.ts' | import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts' | ||||||
| import { Client, ClientOptions } from './client.ts' | import { Client, ClientOptions } from '../client/mod.ts' | ||||||
| import { | import { | ||||||
|   CategoriesManager, |   CategoriesManager, | ||||||
|   Command, |   Command, | ||||||
|  | @ -9,7 +9,7 @@ import { | ||||||
|   CommandsManager, |   CommandsManager, | ||||||
|   parseCommand |   parseCommand | ||||||
| } from './command.ts' | } from './command.ts' | ||||||
| import { Extension, ExtensionsManager } from './extensions.ts' | import { Extension, ExtensionsManager } from './extension.ts' | ||||||
| 
 | 
 | ||||||
| type PrefixReturnType = string | string[] | Promise<string | string[]> | type PrefixReturnType = string | string[] | Promise<string | string[]> | ||||||
| 
 | 
 | ||||||
|  | @ -43,6 +43,11 @@ export interface CommandClientOptions extends ClientOptions { | ||||||
|   caseSensitive?: boolean |   caseSensitive?: boolean | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Harmony Client with extended functionality for Message based Commands parsing and handling. | ||||||
|  |  * | ||||||
|  |  * See SlashClient (`Client#slash`) for more info about Slash Commands. | ||||||
|  |  */ | ||||||
| export class CommandClient extends Client implements CommandClientOptions { | export class CommandClient extends Client implements CommandClientOptions { | ||||||
|   prefix: string | string[] |   prefix: string | string[] | ||||||
|   mentionPrefix: boolean |   mentionPrefix: boolean | ||||||
|  | @ -362,15 +367,19 @@ export class CommandClient extends Client implements CommandClientOptions { | ||||||
|       const result = await command.execute(ctx) |       const result = await command.execute(ctx) | ||||||
|       await command.afterExecute(ctx, result) |       await command.afterExecute(ctx, result) | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       await command |       try { | ||||||
|         .onError(ctx, e) |         await command.onError(ctx, e) | ||||||
|         .catch((e: Error) => this.emit('commandError', ctx, e)) |       } catch (e) { | ||||||
|  |         this.emit('commandError', ctx, e) | ||||||
|  |       } | ||||||
|       this.emit('commandError', ctx, e) |       this.emit('commandError', ctx, e) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** Command decorator */ | /** | ||||||
|  |  * Command decorator. Decorates the function with optional metadata as a Command registered upon constructing class. | ||||||
|  |  */ | ||||||
| export function command(options?: CommandOptions) { | export function command(options?: CommandOptions) { | ||||||
|   return function (target: CommandClient | Extension, name: string) { |   return function (target: CommandClient | Extension, name: string) { | ||||||
|     if (target._decoratedCommands === undefined) target._decoratedCommands = {} |     if (target._decoratedCommands === undefined) target._decoratedCommands = {} | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Message } from '../structures/message.ts' | import type { Message } from '../structures/message.ts' | ||||||
| import { TextChannel } from '../structures/textChannel.ts' | import type { TextChannel } from '../structures/textChannel.ts' | ||||||
| import { User } from '../structures/user.ts' | import type { User } from '../structures/user.ts' | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { CommandClient } from './commandClient.ts' | import type { CommandClient } from './client.ts' | ||||||
| import { Extension } from './extensions.ts' | import type { Extension } from './extension.ts' | ||||||
| import { join, walk } from '../../deps.ts' | import { join, walk } from '../../deps.ts' | ||||||
| 
 | 
 | ||||||
| export interface CommandContext { | export interface CommandContext { | ||||||
|  | @ -72,6 +72,8 @@ export interface CommandOptions { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class Command implements CommandOptions { | export class Command implements CommandOptions { | ||||||
|  |   static meta?: CommandOptions | ||||||
|  | 
 | ||||||
|   name: string = '' |   name: string = '' | ||||||
|   description?: string |   description?: string | ||||||
|   category?: string |   category?: string | ||||||
|  | @ -298,6 +300,8 @@ export class CommandsLoader { | ||||||
|   /** |   /** | ||||||
|    * Load a Command from file. |    * Load a Command from file. | ||||||
|    * |    * | ||||||
|  |    * NOTE: Relative paths resolve from cwd | ||||||
|  |    * | ||||||
|    * @param filePath Path of Command file. |    * @param filePath Path of Command file. | ||||||
|    * @param exportName Export name. Default is the "default" export. |    * @param exportName Export name. Default is the "default" export. | ||||||
|    */ |    */ | ||||||
|  | @ -342,6 +346,8 @@ export class CommandsLoader { | ||||||
|   /** |   /** | ||||||
|    * Load commands from a Directory. |    * Load commands from a Directory. | ||||||
|    * |    * | ||||||
|  |    * NOTE: Relative paths resolve from cwd | ||||||
|  |    * | ||||||
|    * @param path Path of the directory. |    * @param path Path of the directory. | ||||||
|    * @param options Options to configure loading. |    * @param options Options to configure loading. | ||||||
|    */ |    */ | ||||||
|  | @ -486,12 +492,16 @@ export class CommandsManager { | ||||||
| 
 | 
 | ||||||
|   /** Add a Command */ |   /** Add a Command */ | ||||||
|   add(cmd: Command | typeof Command): boolean { |   add(cmd: Command | typeof Command): boolean { | ||||||
|     // eslint-disable-next-line new-cap
 |     if (!(cmd instanceof Command)) { | ||||||
|     if (!(cmd instanceof Command)) cmd = new cmd() |       const CmdClass = cmd | ||||||
|  |       cmd = new CmdClass() | ||||||
|  |       Object.assign(cmd, CmdClass.meta ?? {}) | ||||||
|  |     } | ||||||
|     if (this.exists(cmd, cmd.extension?.subPrefix)) |     if (this.exists(cmd, cmd.extension?.subPrefix)) | ||||||
|       throw new Error( |       throw new Error( | ||||||
|         `Failed to add Command '${cmd.toString()}' with name/alias already exists.` |         `Failed to add Command '${cmd.toString()}' with name/alias already exists.` | ||||||
|       ) |       ) | ||||||
|  |     if (cmd.name === '') throw new Error('Command has no name') | ||||||
|     this.list.set( |     this.list.set( | ||||||
|       `${cmd.name}-${ |       `${cmd.name}-${ | ||||||
|         this.list.filter((e) => |         this.list.filter((e) => | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { ClientEvents } from '../../mod.ts' |  | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { Command } from './command.ts' | import { Command } from './command.ts' | ||||||
| import { CommandClient } from './commandClient.ts' | import { CommandClient } from './client.ts' | ||||||
|  | import type { ClientEvents } from '../gateway/handlers/mod.ts' | ||||||
| 
 | 
 | ||||||
| export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any | export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any | ||||||
| 
 | 
 | ||||||
							
								
								
									
										3
									
								
								src/commands/mod.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/commands/mod.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export * from './client.ts' | ||||||
|  | export * from './command.ts' | ||||||
|  | export * from './extension.ts' | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| export const DISCORD_API_URL: string = 'https://discord.com/api' |  | ||||||
| 
 |  | ||||||
| export const DISCORD_GATEWAY_URL: string = 'wss://gateway.discord.gg' |  | ||||||
| 
 |  | ||||||
| export const DISCORD_CDN_URL: string = 'https://cdn.discordapp.com' |  | ||||||
| 
 |  | ||||||
| export const DISCORD_API_VERSION: number = 8 |  | ||||||
| 
 |  | ||||||
| export const DISCORD_VOICE_VERSION: number = 4 |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { SlashCommand } from '../../models/slashClient.ts' | import { SlashCommand } from '../../interactions/slashCommand.ts' | ||||||
| import { ApplicationCommandPayload } from '../../types/gateway.ts' | import { ApplicationCommandPayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const applicationCommandCreate: GatewayEventHandler = async ( | export const applicationCommandCreate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { SlashCommand } from '../../models/slashClient.ts' | import { SlashCommand } from '../../interactions/slashCommand.ts' | ||||||
| import { ApplicationCommandPayload } from '../../types/gateway.ts' | import { ApplicationCommandPayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const applicationCommandDelete: GatewayEventHandler = async ( | export const applicationCommandDelete: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { SlashCommand } from '../../models/slashClient.ts' | import { SlashCommand } from '../../interactions/slashCommand.ts' | ||||||
| import { ApplicationCommandPayload } from '../../types/gateway.ts' | import { ApplicationCommandPayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const applicationCommandUpdate: GatewayEventHandler = async ( | export const applicationCommandUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,10 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import getChannelByType from '../../utils/getChannelByType.ts' | import getChannelByType from '../../utils/channel.ts' | ||||||
| import { ChannelPayload, GuildChannelPayload } from '../../types/channel.ts' | import type { | ||||||
| import { Guild } from '../../structures/guild.ts' |   ChannelPayload, | ||||||
|  |   GuildChannelPayload | ||||||
|  | } from '../../types/channel.ts' | ||||||
|  | import type { Guild } from '../../structures/guild.ts' | ||||||
| 
 | 
 | ||||||
| export const channelCreate: GatewayEventHandler = async ( | export const channelCreate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { ChannelPayload } from '../../types/channel.ts' | import type { ChannelPayload } from '../../types/channel.ts' | ||||||
| 
 | 
 | ||||||
| export const channelDelete: GatewayEventHandler = async ( | export const channelDelete: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { ChannelPinsUpdatePayload } from '../../types/gateway.ts' | import type { ChannelPinsUpdatePayload } from '../../types/gateway.ts' | ||||||
| 
 | 
 | ||||||
| export const channelPinsUpdate: GatewayEventHandler = async ( | export const channelPinsUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Channel } from '../../structures/channel.ts' | import type { Channel } from '../../structures/channel.ts' | ||||||
| import { ChannelPayload } from '../../types/channel.ts' | import type { ChannelPayload } from '../../types/channel.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const channelUpdate: GatewayEventHandler = async ( | export const channelUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { User } from '../../structures/user.ts' | import { User } from '../../structures/user.ts' | ||||||
| import { GuildBanAddPayload } from '../../types/gateway.ts' | import { GuildBanAddPayload } from '../../types/gateway.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { User } from '../../structures/user.ts' | import { User } from '../../structures/user.ts' | ||||||
| import { GuildBanRemovePayload } from '../../types/gateway.ts' | import { GuildBanRemovePayload } from '../../types/gateway.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildPayload } from '../../types/guild.ts' | import { GuildPayload } from '../../types/guild.ts' | ||||||
| import { GuildChannelPayload } from '../../types/channel.ts' | import { GuildChannelPayload } from '../../types/channel.ts' | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildPayload } from '../../types/guild.ts' | import { GuildPayload } from '../../types/guild.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const guildDelete: GatewayEventHandler = async ( | export const guildDelete: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { Emoji } from '../../structures/emoji.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { EmojiPayload } from '../../types/emoji.ts' | import { EmojiPayload } from '../../types/emoji.ts' | ||||||
| import { GuildEmojiUpdatePayload } from '../../types/gateway.ts' | import { GuildEmojiUpdatePayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const guildEmojiUpdate: GatewayEventHandler = async ( | export const guildEmojiUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildIntegrationsUpdatePayload } from '../../types/gateway.ts' | import { GuildIntegrationsUpdatePayload } from '../../types/gateway.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildMemberAddPayload } from '../../types/gateway.ts' | import { GuildMemberAddPayload } from '../../types/gateway.ts' | ||||||
| import { Member } from '../../structures/member.ts' | import { Member } from '../../structures/member.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { User } from '../../structures/user.ts' | import { User } from '../../structures/user.ts' | ||||||
| import { GuildMemberRemovePayload } from '../../types/gateway.ts' | import { GuildMemberRemovePayload } from '../../types/gateway.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildMemberUpdatePayload } from '../../types/gateway.ts' | import { GuildMemberUpdatePayload } from '../../types/gateway.ts' | ||||||
| import { MemberPayload } from '../../types/guild.ts' | import { MemberPayload } from '../../types/guild.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildMemberChunkPayload } from '../../types/gateway.ts' | import { GuildMemberChunkPayload } from '../../types/gateway.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildRoleCreatePayload } from '../../types/gateway.ts' | import { GuildRoleCreatePayload } from '../../types/gateway.ts' | ||||||
| import { Role } from '../../structures/role.ts' | import { Role } from '../../structures/role.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildRoleDeletePayload } from '../../types/gateway.ts' | import { GuildRoleDeletePayload } from '../../types/gateway.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildRoleUpdatePayload } from '../../types/gateway.ts' | import { GuildRoleUpdatePayload } from '../../types/gateway.ts' | ||||||
| import { Role } from '../../structures/role.ts' | import { Role } from '../../structures/role.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { GuildPayload } from '../../types/guild.ts' | import { GuildPayload } from '../../types/guild.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,29 +1,110 @@ | ||||||
|  | import { Guild } from '../../structures/guild.ts' | ||||||
| import { Member } from '../../structures/member.ts' | import { Member } from '../../structures/member.ts' | ||||||
| import { Interaction } from '../../structures/slash.ts' | import { | ||||||
|  |   Interaction, | ||||||
|  |   InteractionApplicationCommandResolved, | ||||||
|  |   InteractionChannel | ||||||
|  | } from '../../structures/slash.ts' | ||||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||||
| import { InteractionPayload } from '../../types/slash.ts' | import { InteractionPayload } from '../../types/slash.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import { UserPayload } from '../../types/user.ts' | ||||||
|  | import { Permissions } from '../../utils/permissions.ts' | ||||||
|  | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
|  | import { User } from '../../structures/user.ts' | ||||||
|  | import { Role } from '../../structures/role.ts' | ||||||
| 
 | 
 | ||||||
| export const interactionCreate: GatewayEventHandler = async ( | export const interactionCreate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|   d: InteractionPayload |   d: InteractionPayload | ||||||
| ) => { | ) => { | ||||||
|   const guild = await gateway.client.guilds.get(d.guild_id) |   // NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction.
 | ||||||
|   if (guild === undefined) return |   // This case can be seen in future proofing Interactions, and one he mentioned was
 | ||||||
|  |   // that bots will be able to add custom context menus. In that case, Interaction will not have it.
 | ||||||
|  |   // Ref: https://github.com/discord/discord-api-docs/pull/2568/files#r569025697
 | ||||||
|  |   if (d.channel_id === undefined) return | ||||||
| 
 | 
 | ||||||
|   await guild.members.set(d.member.user.id, d.member) |   const guild = | ||||||
|   const member = ((await guild.members.get( |     d.guild_id === undefined | ||||||
|     d.member.user.id |       ? undefined | ||||||
|   )) as unknown) as Member |       : await gateway.client.guilds.get(d.guild_id) | ||||||
|  | 
 | ||||||
|  |   if (d.member !== undefined) | ||||||
|  |     await guild?.members.set(d.member.user.id, d.member) | ||||||
|  |   const member = | ||||||
|  |     d.member !== undefined | ||||||
|  |       ? (((await guild?.members.get(d.member.user.id)) as unknown) as Member) | ||||||
|  |       : undefined | ||||||
|  |   if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user) | ||||||
|  |   const dmUser = | ||||||
|  |     d.user !== undefined ? await gateway.client.users.get(d.user.id) : undefined | ||||||
|  | 
 | ||||||
|  |   const user = member !== undefined ? member.user : dmUser | ||||||
|  |   if (user === undefined) return | ||||||
| 
 | 
 | ||||||
|   const channel = |   const channel = | ||||||
|     (await gateway.client.channels.get<GuildTextBasedChannel>(d.channel_id)) ?? |     (await gateway.client.channels.get<GuildTextBasedChannel>(d.channel_id)) ?? | ||||||
|     (await gateway.client.channels.fetch<GuildTextBasedChannel>(d.channel_id)) |     (await gateway.client.channels.fetch<GuildTextBasedChannel>(d.channel_id)) | ||||||
| 
 | 
 | ||||||
|  |   const resolved: InteractionApplicationCommandResolved = { | ||||||
|  |     users: {}, | ||||||
|  |     channels: {}, | ||||||
|  |     members: {}, | ||||||
|  |     roles: {} | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (d.data?.resolved !== undefined) { | ||||||
|  |     for (const [id, data] of Object.entries(d.data.resolved.users ?? {})) { | ||||||
|  |       await gateway.client.users.set(id, data) | ||||||
|  |       resolved.users[id] = ((await gateway.client.users.get( | ||||||
|  |         id | ||||||
|  |       )) as unknown) as User | ||||||
|  |       if (resolved.members[id] !== undefined) | ||||||
|  |         resolved.users[id].member = resolved.members[id] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const [id, data] of Object.entries(d.data.resolved.members ?? {})) { | ||||||
|  |       const roles = await guild?.roles.array() | ||||||
|  |       let permissions = new Permissions(Permissions.DEFAULT) | ||||||
|  |       if (roles !== undefined) { | ||||||
|  |         const mRoles = roles.filter( | ||||||
|  |           (r) => (data?.roles?.includes(r.id) as boolean) || r.id === guild?.id | ||||||
|  |         ) | ||||||
|  |         permissions = new Permissions(mRoles.map((r) => r.permissions)) | ||||||
|  |       } | ||||||
|  |       data.user = (d.data.resolved.users?.[id] as unknown) as UserPayload | ||||||
|  |       resolved.members[id] = new Member( | ||||||
|  |         gateway.client, | ||||||
|  |         data, | ||||||
|  |         resolved.users[id], | ||||||
|  |         guild as Guild, | ||||||
|  |         permissions | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const [id, data] of Object.entries(d.data.resolved.roles ?? {})) { | ||||||
|  |       if (guild !== undefined) { | ||||||
|  |         await guild.roles.set(id, data) | ||||||
|  |         resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role | ||||||
|  |       } else { | ||||||
|  |         resolved.roles[id] = new Role( | ||||||
|  |           gateway.client, | ||||||
|  |           data, | ||||||
|  |           (guild as unknown) as Guild | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const [id, data] of Object.entries(d.data.resolved.channels ?? {})) { | ||||||
|  |       resolved.channels[id] = new InteractionChannel(gateway.client, data) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   const interaction = new Interaction(gateway.client, d, { |   const interaction = new Interaction(gateway.client, d, { | ||||||
|     member, |     member, | ||||||
|     guild, |     guild, | ||||||
|     channel |     channel, | ||||||
|  |     user, | ||||||
|  |     resolved | ||||||
|   }) |   }) | ||||||
|   gateway.client.emit('interactionCreate', interaction) |   gateway.client.emit('interactionCreate', interaction) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { InviteCreatePayload } from '../../types/gateway.ts' | import { InviteCreatePayload } from '../../types/gateway.ts' | ||||||
| import { ChannelPayload } from '../../types/channel.ts' | import { ChannelPayload } from '../../types/channel.ts' | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import { Guild } from '../../structures/guild.ts' | ||||||
| import { InviteDeletePayload } from '../../types/gateway.ts' | import { InviteDeletePayload } from '../../types/gateway.ts' | ||||||
| import { PartialInvitePayload } from '../../types/invite.ts' | import { PartialInvitePayload } from '../../types/invite.ts' | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Message } from '../../structures/message.ts' | import { Message } from '../../structures/message.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { User } from '../../structures/user.ts' | import { User } from '../../structures/user.ts' | ||||||
| import { MessagePayload } from '../../types/channel.ts' | import type { MessagePayload } from '../../types/channel.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const messageCreate: GatewayEventHandler = async ( | export const messageCreate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { MessageDeletePayload } from '../../types/gateway.ts' | import type { MessageDeletePayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const messageDelete: GatewayEventHandler = async ( | export const messageDelete: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Message } from '../../structures/message.ts' | import type { Message } from '../../structures/message.ts' | ||||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||||
| import { MessageDeleteBulkPayload } from '../../types/gateway.ts' | import type { MessageDeleteBulkPayload } from '../../types/gateway.ts' | ||||||
| import { Collection } from '../../utils/collection.ts' | import { Collection } from '../../utils/collection.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const messageDeleteBulk: GatewayEventHandler = async ( | export const messageDeleteBulk: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { MessageReactionAddPayload } from '../../types/gateway.ts' | import type { MessageReactionAddPayload } from '../../types/gateway.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { MessageReaction } from '../../structures/messageReaction.ts' | import type { MessageReaction } from '../../structures/messageReaction.ts' | ||||||
| import { UserPayload } from '../../types/user.ts' | import type { UserPayload } from '../../types/user.ts' | ||||||
| 
 | 
 | ||||||
| export const messageReactionAdd: GatewayEventHandler = async ( | export const messageReactionAdd: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { MessageReactionRemovePayload } from '../../types/gateway.ts' | import type { MessageReactionRemovePayload } from '../../types/gateway.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| 
 | 
 | ||||||
| export const messageReactionRemove: GatewayEventHandler = async ( | export const messageReactionRemove: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { MessageReactionRemoveAllPayload } from '../../types/gateway.ts' | import type { MessageReactionRemoveAllPayload } from '../../types/gateway.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| 
 | 
 | ||||||
| export const messageReactionRemoveAll: GatewayEventHandler = async ( | export const messageReactionRemoveAll: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts' | import type { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| 
 | 
 | ||||||
| export const messageReactionRemoveEmoji: GatewayEventHandler = async ( | export const messageReactionRemoveEmoji: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Message } from '../../structures/message.ts' | import type { Message } from '../../structures/message.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { MessagePayload } from '../../types/channel.ts' | import type { MessagePayload } from '../../types/channel.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const messageUpdate: GatewayEventHandler = async ( | export const messageUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { GatewayEventHandler } from '../index.ts' | import type { GatewayEventHandler } from '../mod.ts' | ||||||
| import { | import type { | ||||||
|   GatewayEvents, |   GatewayEvents, | ||||||
|   MessageDeletePayload, |   MessageDeletePayload, | ||||||
|   TypingStartGuildData |   TypingStartGuildData | ||||||
|  | @ -31,18 +31,18 @@ import { webhooksUpdate } from './webhooksUpdate.ts' | ||||||
| import { messageDeleteBulk } from './messageDeleteBulk.ts' | import { messageDeleteBulk } from './messageDeleteBulk.ts' | ||||||
| import { userUpdate } from './userUpdate.ts' | import { userUpdate } from './userUpdate.ts' | ||||||
| import { typingStart } from './typingStart.ts' | import { typingStart } from './typingStart.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import type { Guild } from '../../structures/guild.ts' | ||||||
| import { User } from '../../structures/user.ts' | import type { User } from '../../structures/user.ts' | ||||||
| import { Emoji } from '../../structures/emoji.ts' | import type { Emoji } from '../../structures/emoji.ts' | ||||||
| import { Member } from '../../structures/member.ts' | import type { Member } from '../../structures/member.ts' | ||||||
| import { Role } from '../../structures/role.ts' | import type { Role } from '../../structures/role.ts' | ||||||
| import { Message } from '../../structures/message.ts' | import type { Message } from '../../structures/message.ts' | ||||||
| import { Collection } from '../../utils/collection.ts' | import type { Collection } from '../../utils/collection.ts' | ||||||
| import { voiceServerUpdate } from './voiceServerUpdate.ts' | import { voiceServerUpdate } from './voiceServerUpdate.ts' | ||||||
| import { voiceStateUpdate } from './voiceStateUpdate.ts' | import { voiceStateUpdate } from './voiceStateUpdate.ts' | ||||||
| import { VoiceState } from '../../structures/voiceState.ts' | import type { VoiceState } from '../../structures/voiceState.ts' | ||||||
| import { messageReactionAdd } from './messageReactionAdd.ts' | import { messageReactionAdd } from './messageReactionAdd.ts' | ||||||
| import { messageReactionRemove } from './messageReactionRemove.ts' | import { messageReactionRemove } from './messageReactionRemove.ts' | ||||||
| import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts' | import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts' | ||||||
|  | @ -51,23 +51,23 @@ import { guildMembersChunk } from './guildMembersChunk.ts' | ||||||
| import { presenceUpdate } from './presenceUpdate.ts' | import { presenceUpdate } from './presenceUpdate.ts' | ||||||
| import { inviteCreate } from './inviteCreate.ts' | import { inviteCreate } from './inviteCreate.ts' | ||||||
| import { inviteDelete } from './inviteDelete.ts' | import { inviteDelete } from './inviteDelete.ts' | ||||||
| import { MessageReaction } from '../../structures/messageReaction.ts' | import type { MessageReaction } from '../../structures/messageReaction.ts' | ||||||
| import { Invite } from '../../structures/invite.ts' | import type { Invite } from '../../structures/invite.ts' | ||||||
| import { Presence } from '../../structures/presence.ts' | import type { Presence } from '../../structures/presence.ts' | ||||||
| import { | import type { | ||||||
|   EveryChannelTypes, |   EveryChannelTypes, | ||||||
|   EveryTextChannelTypes |   EveryTextChannelTypes | ||||||
| } from '../../utils/getChannelByType.ts' | } from '../../utils/channel.ts' | ||||||
| import { interactionCreate } from './interactionCreate.ts' | import { interactionCreate } from './interactionCreate.ts' | ||||||
| import { Interaction } from '../../structures/slash.ts' | import type { Interaction } from '../../structures/slash.ts' | ||||||
| import { CommandContext } from '../../models/command.ts' | import type { CommandContext } from '../../commands/command.ts' | ||||||
| import { RequestMethods } from '../../models/rest.ts' | import type { RequestMethods } from '../../rest/types.ts' | ||||||
| import { PartialInvitePayload } from '../../types/invite.ts' | import type { PartialInvitePayload } from '../../types/invite.ts' | ||||||
| import { GuildChannels } from '../../types/guild.ts' | import type { GuildChannels } from '../../types/guild.ts' | ||||||
| import { applicationCommandCreate } from './applicationCommandCreate.ts' | import { applicationCommandCreate } from './applicationCommandCreate.ts' | ||||||
| import { applicationCommandDelete } from './applicationCommandDelete.ts' | import { applicationCommandDelete } from './applicationCommandDelete.ts' | ||||||
| import { applicationCommandUpdate } from './applicationCommandUpdate.ts' | import { applicationCommandUpdate } from './applicationCommandUpdate.ts' | ||||||
| import { SlashCommand } from '../../models/slashClient.ts' | import type { SlashCommand } from '../../interactions/slashCommand.ts' | ||||||
| 
 | 
 | ||||||
| export const gatewayHandlers: { | export const gatewayHandlers: { | ||||||
|   [eventCode in GatewayEvents]: GatewayEventHandler | undefined |   [eventCode in GatewayEvents]: GatewayEventHandler | undefined | ||||||
|  | @ -393,7 +393,15 @@ export type ClientEvents = { | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
|   guildMembersChunked: [guild: Guild, chunks: number] |   guildMembersChunked: [guild: Guild, chunks: number] | ||||||
|   rateLimit: [data: { method: RequestMethods; url: string; body: any }] |   rateLimit: [ | ||||||
|  |     data: { | ||||||
|  |       method: RequestMethods | ||||||
|  |       path: string | ||||||
|  |       global: boolean | ||||||
|  |       timeout: number | ||||||
|  |       limit: number | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|   inviteDeleteUncached: [invite: PartialInvitePayload] |   inviteDeleteUncached: [invite: PartialInvitePayload] | ||||||
|   voiceStateRemoveUncached: [data: { guild: Guild; member: Member }] |   voiceStateRemoveUncached: [data: { guild: Guild; member: Member }] | ||||||
|   userUpdateUncached: [user: User] |   userUpdateUncached: [user: User] | ||||||
|  | @ -414,4 +422,5 @@ export type ClientEvents = { | ||||||
|   commandMissingArgs: [ctx: CommandContext] |   commandMissingArgs: [ctx: CommandContext] | ||||||
|   commandUsed: [ctx: CommandContext] |   commandUsed: [ctx: CommandContext] | ||||||
|   commandError: [ctx: CommandContext, err: Error] |   commandError: [ctx: CommandContext, err: Error] | ||||||
|  |   gatewayError: [err: ErrorEvent, shards: [number, number]] | ||||||
| } | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { PresenceUpdatePayload } from '../../types/gateway.ts' | import type { PresenceUpdatePayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const presenceUpdate: GatewayEventHandler = async ( | export const presenceUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { User } from '../../structures/user.ts' | import { User } from '../../structures/user.ts' | ||||||
| import { Ready } from '../../types/gateway.ts' | import type { Ready } from '../../types/gateway.ts' | ||||||
| import { GuildPayload } from '../../types/guild.ts' | import type { GuildPayload } from '../../types/guild.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const ready: GatewayEventHandler = async ( | export const ready: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const reconnect: GatewayEventHandler = async ( | export const reconnect: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { User } from '../../structures/user.ts' | import { User } from '../../structures/user.ts' | ||||||
| import { CLIENT_USER } from '../../types/endpoint.ts' | import { CLIENT_USER } from '../../types/endpoint.ts' | ||||||
| import { Resume } from '../../types/gateway.ts' | import type { Resume } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const resume: GatewayEventHandler = async ( | export const resume: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Member } from '../../structures/member.ts' | import { Member } from '../../structures/member.ts' | ||||||
| import { TextChannel } from '../../structures/textChannel.ts' | import type { TextChannel } from '../../structures/textChannel.ts' | ||||||
| import { TypingStartPayload } from '../../types/gateway.ts' | import type { TypingStartPayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| // TODO: Do we need to add uncached events here?
 | // TODO: Do we need to add uncached events here?
 | ||||||
| export const typingStart: GatewayEventHandler = async ( | export const typingStart: GatewayEventHandler = async ( | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { User } from '../../structures/user.ts' | import type { User } from '../../structures/user.ts' | ||||||
| import { UserPayload } from '../../types/user.ts' | import type { UserPayload } from '../../types/user.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const userUpdate: GatewayEventHandler = async ( | export const userUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Guild } from '../../structures/guild.ts' | import type { Guild } from '../../structures/guild.ts' | ||||||
| import { VoiceServerUpdatePayload } from '../../types/gateway.ts' | import type { VoiceServerUpdatePayload } from '../../types/gateway.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const voiceServerUpdate: GatewayEventHandler = async ( | export const voiceServerUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Guild } from '../../structures/guild.ts' | import type { Guild } from '../../structures/guild.ts' | ||||||
| import { VoiceState } from '../../structures/voiceState.ts' | import type { VoiceState } from '../../structures/voiceState.ts' | ||||||
| import { MemberPayload } from '../../types/guild.ts' | import type { MemberPayload } from '../../types/guild.ts' | ||||||
| import { VoiceStatePayload } from '../../types/voice.ts' | import type { VoiceStatePayload } from '../../types/voice.ts' | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| 
 | 
 | ||||||
| export const voiceStateUpdate: GatewayEventHandler = async ( | export const voiceStateUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||||
| import { Guild } from '../../structures/guild.ts' | import type { Guild } from '../../structures/guild.ts' | ||||||
| import { WebhooksUpdatePayload } from '../../types/gateway.ts' | import type { WebhooksUpdatePayload } from '../../types/gateway.ts' | ||||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||||
| 
 | 
 | ||||||
| export const webhooksUpdate: GatewayEventHandler = async ( | export const webhooksUpdate: GatewayEventHandler = async ( | ||||||
|   gateway: Gateway, |   gateway: Gateway, | ||||||
|  |  | ||||||
|  | @ -1,24 +1,21 @@ | ||||||
| import { unzlib } from '../../deps.ts' | import { unzlib } from '../../deps.ts' | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { |  | ||||||
|   DISCORD_GATEWAY_URL, |  | ||||||
|   DISCORD_API_VERSION |  | ||||||
| } from '../consts/urlsAndVersions.ts' |  | ||||||
| import { GatewayResponse } from '../types/gatewayResponse.ts' | import { GatewayResponse } from '../types/gatewayResponse.ts' | ||||||
| import { | import { | ||||||
|   GatewayOpcodes, |   GatewayOpcodes, | ||||||
|   GatewayIntents, |  | ||||||
|   GatewayCloseCodes, |   GatewayCloseCodes, | ||||||
|   IdentityPayload, |   IdentityPayload, | ||||||
|   StatusUpdatePayload, |   StatusUpdatePayload, | ||||||
|   GatewayEvents |   GatewayEvents | ||||||
| } from '../types/gateway.ts' | } from '../types/gateway.ts' | ||||||
| import { gatewayHandlers } from './handlers/index.ts' | import { gatewayHandlers } from './handlers/mod.ts' | ||||||
| import { GatewayCache } from '../managers/gatewayCache.ts' | import { GatewayCache } from '../managers/gatewayCache.ts' | ||||||
| import { delay } from '../utils/delay.ts' | import { delay } from '../utils/delay.ts' | ||||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
|  | import { decodeText } from '../utils/encoding.ts' | ||||||
|  | import { Constants } from '../types/constants.ts' | ||||||
| 
 | 
 | ||||||
| export interface RequestMembersOptions { | export interface RequestMembersOptions { | ||||||
|   limit?: number |   limit?: number | ||||||
|  | @ -57,8 +54,6 @@ export type GatewayTypedEvents = { | ||||||
|  */ |  */ | ||||||
| export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   websocket?: WebSocket |   websocket?: WebSocket | ||||||
|   token?: string |  | ||||||
|   intents?: GatewayIntents[] |  | ||||||
|   connected = false |   connected = false | ||||||
|   initialized = false |   initialized = false | ||||||
|   heartbeatInterval = 0 |   heartbeatInterval = 0 | ||||||
|  | @ -71,6 +66,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   cache: GatewayCache |   cache: GatewayCache | ||||||
|   private timedIdentify: number | null = null |   private timedIdentify: number | null = null | ||||||
|   shards?: number[] |   shards?: number[] | ||||||
|  |   ping: number = 0 | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, shards?: number[]) { |   constructor(client: Client, shards?: number[]) { | ||||||
|     super() |     super() | ||||||
|  | @ -92,7 +88,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     } |     } | ||||||
|     if (data instanceof Uint8Array) { |     if (data instanceof Uint8Array) { | ||||||
|       data = unzlib(data) |       data = unzlib(data) | ||||||
|       data = new TextDecoder('utf-8').decode(data) |       data = decodeText(data) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const { op, d, s, t }: GatewayResponse = JSON.parse(data) |     const { op, d, s, t }: GatewayResponse = JSON.parse(data) | ||||||
|  | @ -120,11 +116,9 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
| 
 | 
 | ||||||
|       case GatewayOpcodes.HEARTBEAT_ACK: |       case GatewayOpcodes.HEARTBEAT_ACK: | ||||||
|         this.heartbeatServerResponded = true |         this.heartbeatServerResponded = true | ||||||
|         this.client.ping = Date.now() - this.lastPingTimestamp |         this.ping = Date.now() - this.lastPingTimestamp | ||||||
|         this.emit('ping', this.client.ping) |         this.emit('ping', this.ping) | ||||||
|         this.debug( |         this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.ping}ms`) | ||||||
|           `Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms` |  | ||||||
|         ) |  | ||||||
|         break |         break | ||||||
| 
 | 
 | ||||||
|       case GatewayOpcodes.INVALID_SESSION: |       case GatewayOpcodes.INVALID_SESSION: | ||||||
|  | @ -157,7 +151,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
| 
 | 
 | ||||||
|           const handler = gatewayHandlers[t] |           const handler = gatewayHandlers[t] | ||||||
| 
 | 
 | ||||||
|           if (handler !== undefined) { |           if (handler !== undefined && d !== null) { | ||||||
|             handler(this, d) |             handler(this, d) | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  | @ -177,8 +171,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|       } |       } | ||||||
|       case GatewayOpcodes.RECONNECT: { |       case GatewayOpcodes.RECONNECT: { | ||||||
|         this.emit('reconnectRequired') |         this.emit('reconnectRequired') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         this.debug('Received OpCode RECONNECT') | ||||||
|         this.reconnect() |         await this.reconnect() | ||||||
|         break |         break | ||||||
|       } |       } | ||||||
|       default: |       default: | ||||||
|  | @ -194,8 +188,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     switch (code) { |     switch (code) { | ||||||
|       case GatewayCloseCodes.UNKNOWN_ERROR: |       case GatewayCloseCodes.UNKNOWN_ERROR: | ||||||
|         this.debug('API has encountered Unknown Error. Reconnecting...') |         this.debug('API has encountered Unknown Error. Reconnecting...') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect() | ||||||
|         this.reconnect() |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.UNKNOWN_OPCODE: |       case GatewayCloseCodes.UNKNOWN_OPCODE: | ||||||
|         throw new Error( |         throw new Error( | ||||||
|  | @ -209,20 +202,17 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|         throw new Error('Invalid Token provided!') |         throw new Error('Invalid Token provided!') | ||||||
|       case GatewayCloseCodes.INVALID_SEQ: |       case GatewayCloseCodes.INVALID_SEQ: | ||||||
|         this.debug('Invalid Seq was sent. Reconnecting.') |         this.debug('Invalid Seq was sent. Reconnecting.') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect() | ||||||
|         this.reconnect() |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.RATE_LIMITED: |       case GatewayCloseCodes.RATE_LIMITED: | ||||||
|         throw new Error("You're ratelimited. Calm down.") |         throw new Error("You're ratelimited. Calm down.") | ||||||
|       case GatewayCloseCodes.SESSION_TIMED_OUT: |       case GatewayCloseCodes.SESSION_TIMED_OUT: | ||||||
|         this.debug('Session Timeout. Reconnecting.') |         this.debug('Session Timeout. Reconnecting.') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect(true) | ||||||
|         this.reconnect(true) |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.INVALID_SHARD: |       case GatewayCloseCodes.INVALID_SHARD: | ||||||
|         this.debug('Invalid Shard was sent. Reconnecting.') |         this.debug('Invalid Shard was sent. Reconnecting.') | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         await this.reconnect() | ||||||
|         this.reconnect() |  | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.SHARDING_REQUIRED: |       case GatewayCloseCodes.SHARDING_REQUIRED: | ||||||
|         throw new Error("Couldn't connect. Sharding is required!") |         throw new Error("Couldn't connect. Sharding is required!") | ||||||
|  | @ -260,6 +250,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     error.name = 'ErrorEvent' |     error.name = 'ErrorEvent' | ||||||
|     console.log(error) |     console.log(error) | ||||||
|     this.emit('error', error, event) |     this.emit('error', error, event) | ||||||
|  |     this.client.emit('gatewayError', event, this.shards) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private enqueueIdentify(forceNew?: boolean): void { |   private enqueueIdentify(forceNew?: boolean): void { | ||||||
|  | @ -269,25 +260,11 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async sendIdentify(forceNewSession?: boolean): Promise<void> { |   private async sendIdentify(forceNewSession?: boolean): Promise<void> { | ||||||
|     if (typeof this.token !== 'string') throw new Error('Token not specified') |     if (typeof this.client.token !== 'string') | ||||||
|     if (typeof this.intents !== 'object') |       throw new Error('Token not specified') | ||||||
|  |     if (typeof this.client.intents !== 'object') | ||||||
|       throw new Error('Intents not specified') |       throw new Error('Intents not specified') | ||||||
| 
 | 
 | ||||||
|     if (this.client.fetchGatewayInfo === true) { |  | ||||||
|       this.debug('Fetching /gateway/bot...') |  | ||||||
|       const info = await this.client.rest.api.gateway.bot.get() |  | ||||||
|       if (info.session_start_limit.remaining === 0) |  | ||||||
|         throw new Error( |  | ||||||
|           `Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms` |  | ||||||
|         ) |  | ||||||
|       this.debug(`Recommended Shards: ${info.shards}`) |  | ||||||
|       this.debug('=== Session Limit Info ===') |  | ||||||
|       this.debug( |  | ||||||
|         `Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}` |  | ||||||
|       ) |  | ||||||
|       this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (forceNewSession === undefined || !forceNewSession) { |     if (forceNewSession === undefined || !forceNewSession) { | ||||||
|       const sessionIDCached = await this.cache.get( |       const sessionIDCached = await this.cache.get( | ||||||
|         `session_id_${this.shards?.join('-') ?? '0'}` |         `session_id_${this.shards?.join('-') ?? '0'}` | ||||||
|  | @ -300,7 +277,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const payload: IdentityPayload = { |     const payload: IdentityPayload = { | ||||||
|       token: this.token, |       token: this.client.token, | ||||||
|       properties: { |       properties: { | ||||||
|         $os: this.client.clientProperties.os ?? Deno.build.os, |         $os: this.client.clientProperties.os ?? Deno.build.os, | ||||||
|         $browser: this.client.clientProperties.browser ?? 'harmony', |         $browser: this.client.clientProperties.browser ?? 'harmony', | ||||||
|  | @ -311,7 +288,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|         this.shards === undefined |         this.shards === undefined | ||||||
|           ? [0, 1] |           ? [0, 1] | ||||||
|           : [this.shards[0] ?? 0, this.shards[1] ?? 1], |           : [this.shards[0] ?? 0, this.shards[1] ?? 1], | ||||||
|       intents: this.intents.reduce( |       intents: this.client.intents.reduce( | ||||||
|         (previous, current) => previous | current, |         (previous, current) => previous | current, | ||||||
|         0 |         0 | ||||||
|       ), |       ), | ||||||
|  | @ -327,9 +304,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async sendResume(): Promise<void> { |   private async sendResume(): Promise<void> { | ||||||
|     if (typeof this.token !== 'string') throw new Error('Token not specified') |     if (typeof this.client.token !== 'string') | ||||||
|     if (typeof this.intents !== 'object') |       throw new Error('Token not specified') | ||||||
|       throw new Error('Intents not specified') |  | ||||||
| 
 | 
 | ||||||
|     if (this.sessionID === undefined) { |     if (this.sessionID === undefined) { | ||||||
|       this.sessionID = await this.cache.get( |       this.sessionID = await this.cache.get( | ||||||
|  | @ -348,7 +324,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     const resumePayload = { |     const resumePayload = { | ||||||
|       op: GatewayOpcodes.RESUME, |       op: GatewayOpcodes.RESUME, | ||||||
|       d: { |       d: { | ||||||
|         token: this.token, |         token: this.client.token, | ||||||
|         session_id: this.sessionID, |         session_id: this.sessionID, | ||||||
|         seq: this.sequenceID ?? null |         seq: this.sequenceID ?? null | ||||||
|       } |       } | ||||||
|  | @ -393,8 +369,18 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|             : typeof channel === 'string' |             : typeof channel === 'string' | ||||||
|             ? channel |             ? channel | ||||||
|             : channel?.id, |             : channel?.id, | ||||||
|         self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute, |         self_mute: | ||||||
|         self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf |           channel === undefined | ||||||
|  |             ? undefined | ||||||
|  |             : voiceOptions.mute === undefined | ||||||
|  |             ? false | ||||||
|  |             : voiceOptions.mute, | ||||||
|  |         self_deaf: | ||||||
|  |           channel === undefined | ||||||
|  |             ? undefined | ||||||
|  |             : voiceOptions.deaf === undefined | ||||||
|  |             ? false | ||||||
|  |             : voiceOptions.deaf | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | @ -405,6 +391,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
| 
 | 
 | ||||||
|   async reconnect(forceNew?: boolean): Promise<void> { |   async reconnect(forceNew?: boolean): Promise<void> { | ||||||
|     this.emit('reconnecting') |     this.emit('reconnecting') | ||||||
|  |     this.debug('Reconnecting... (force new: ' + String(forceNew) + ')') | ||||||
| 
 | 
 | ||||||
|     clearInterval(this.heartbeatIntervalID) |     clearInterval(this.heartbeatIntervalID) | ||||||
|     if (forceNew === true) { |     if (forceNew === true) { | ||||||
|  | @ -421,7 +408,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|     this.debug('Initializing WebSocket...') |     this.debug('Initializing WebSocket...') | ||||||
|     this.websocket = new WebSocket( |     this.websocket = new WebSocket( | ||||||
|       // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
 |       // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
 | ||||||
|       `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, |       `${Constants.DISCORD_GATEWAY_URL}/?v=${Constants.DISCORD_API_VERSION}&encoding=json`, | ||||||
|       [] |       [] | ||||||
|     ) |     ) | ||||||
|     this.websocket.binaryType = 'arraybuffer' |     this.websocket.binaryType = 'arraybuffer' | ||||||
|  | @ -432,6 +419,11 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   close(code: number = 1000, reason?: string): void { |   close(code: number = 1000, reason?: string): void { | ||||||
|  |     this.debug( | ||||||
|  |       `Closing with code ${code}${ | ||||||
|  |         reason !== undefined && reason !== '' ? ` and reason ${reason}` : '' | ||||||
|  |       }` | ||||||
|  |     ) | ||||||
|     return this.websocket?.close(code, reason) |     return this.websocket?.close(code, reason) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
							
								
								
									
										3
									
								
								src/interactions/mod.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/interactions/mod.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | export * from './slashClient.ts' | ||||||
|  | export * from './slashModule.ts' | ||||||
|  | export * from './slashCommand.ts' | ||||||
							
								
								
									
										440
									
								
								src/interactions/slashClient.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										440
									
								
								src/interactions/slashClient.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,440 @@ | ||||||
|  | import { | ||||||
|  |   Interaction, | ||||||
|  |   InteractionApplicationCommandResolved | ||||||
|  | } from '../structures/slash.ts' | ||||||
|  | import { | ||||||
|  |   InteractionPayload, | ||||||
|  |   InteractionResponsePayload, | ||||||
|  |   InteractionType, | ||||||
|  |   SlashCommandOptionType | ||||||
|  | } from '../types/slash.ts' | ||||||
|  | import type { Client } from '../client/mod.ts' | ||||||
|  | import { RESTManager } from '../rest/mod.ts' | ||||||
|  | import { SlashModule } from './slashModule.ts' | ||||||
|  | import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' | ||||||
|  | import { User } from '../structures/user.ts' | ||||||
|  | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
|  | import { encodeText, decodeText } from '../utils/encoding.ts' | ||||||
|  | import { SlashCommandsManager } from './slashCommand.ts' | ||||||
|  | 
 | ||||||
|  | export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown | ||||||
|  | export interface SlashCommandHandler { | ||||||
|  |   name: string | ||||||
|  |   guild?: string | ||||||
|  |   parent?: string | ||||||
|  |   group?: string | ||||||
|  |   handler: SlashCommandHandlerCallback | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Options for SlashClient */ | ||||||
|  | export interface SlashOptions { | ||||||
|  |   id?: string | (() => string) | ||||||
|  |   client?: Client | ||||||
|  |   enabled?: boolean | ||||||
|  |   token?: string | ||||||
|  |   rest?: RESTManager | ||||||
|  |   publicKey?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||||
|  | export type SlashClientEvents = { | ||||||
|  |   interaction: [Interaction] | ||||||
|  |   interactionError: [Error] | ||||||
|  |   ping: [] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Slash Client represents an Interactions Client which can be used without Harmony Client. */ | ||||||
|  | export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | ||||||
|  |   id: string | (() => string) | ||||||
|  |   client?: Client | ||||||
|  |   token?: string | ||||||
|  |   enabled: boolean = true | ||||||
|  |   commands: SlashCommandsManager | ||||||
|  |   handlers: SlashCommandHandler[] = [] | ||||||
|  |   rest: RESTManager | ||||||
|  |   modules: SlashModule[] = [] | ||||||
|  |   publicKey?: string | ||||||
|  | 
 | ||||||
|  |   _decoratedSlash?: Array<{ | ||||||
|  |     name: string | ||||||
|  |     guild?: string | ||||||
|  |     parent?: string | ||||||
|  |     group?: string | ||||||
|  |     handler: (interaction: Interaction) => any | ||||||
|  |   }> | ||||||
|  | 
 | ||||||
|  |   constructor(options: SlashOptions) { | ||||||
|  |     super() | ||||||
|  |     let id = options.id | ||||||
|  |     if (options.token !== undefined) id = atob(options.token?.split('.')[0]) | ||||||
|  |     if (id === undefined) | ||||||
|  |       throw new Error('ID could not be found. Pass at least client or token') | ||||||
|  |     this.id = id | ||||||
|  |     this.client = options.client | ||||||
|  |     this.token = options.token | ||||||
|  |     this.publicKey = options.publicKey | ||||||
|  | 
 | ||||||
|  |     this.enabled = options.enabled ?? true | ||||||
|  | 
 | ||||||
|  |     if (this.client?._decoratedSlash !== undefined) { | ||||||
|  |       this.client._decoratedSlash.forEach((e) => { | ||||||
|  |         e.handler = e.handler.bind(this.client) | ||||||
|  |         this.handlers.push(e) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this._decoratedSlash !== undefined) { | ||||||
|  |       this._decoratedSlash.forEach((e) => { | ||||||
|  |         e.handler = e.handler.bind(this.client) | ||||||
|  |         this.handlers.push(e) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.rest = | ||||||
|  |       options.client === undefined | ||||||
|  |         ? options.rest === undefined | ||||||
|  |           ? new RESTManager({ | ||||||
|  |               token: this.token | ||||||
|  |             }) | ||||||
|  |           : options.rest | ||||||
|  |         : options.client.rest | ||||||
|  | 
 | ||||||
|  |     this.client?.on( | ||||||
|  |       'interactionCreate', | ||||||
|  |       async (interaction) => await this._process(interaction) | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     this.commands = new SlashCommandsManager(this) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getID(): string { | ||||||
|  |     return typeof this.id === 'string' ? this.id : this.id() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Adds a new Slash Command Handler */ | ||||||
|  |   handle( | ||||||
|  |     cmd: string | SlashCommandHandler, | ||||||
|  |     handler?: SlashCommandHandlerCallback | ||||||
|  |   ): SlashClient { | ||||||
|  |     const handle = { | ||||||
|  |       name: typeof cmd === 'string' ? cmd : cmd.name, | ||||||
|  |       ...(handler !== undefined ? { handler } : {}), | ||||||
|  |       ...(typeof cmd === 'string' ? {} : cmd) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (handle.handler === undefined) | ||||||
|  |       throw new Error('Invalid usage. Handler function not provided') | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       typeof handle.name === 'string' && | ||||||
|  |       handle.name.includes(' ') && | ||||||
|  |       handle.parent === undefined && | ||||||
|  |       handle.group === undefined | ||||||
|  |     ) { | ||||||
|  |       const parts = handle.name.split(/ +/).filter((e) => e !== '') | ||||||
|  |       if (parts.length > 3 || parts.length < 1) | ||||||
|  |         throw new Error('Invalid command name') | ||||||
|  |       const root = parts.shift() as string | ||||||
|  |       const group = parts.length === 2 ? parts.shift() : undefined | ||||||
|  |       const sub = parts.shift() | ||||||
|  | 
 | ||||||
|  |       handle.name = sub ?? root | ||||||
|  |       handle.group = group | ||||||
|  |       handle.parent = sub === undefined ? undefined : root | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.handlers.push(handle as any) | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Load a Slash Module */ | ||||||
|  |   loadModule(module: SlashModule): SlashClient { | ||||||
|  |     this.modules.push(module) | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Get all Handlers. Including Slash Modules */ | ||||||
|  |   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 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */ | ||||||
|  |   private _getCommand(i: Interaction): SlashCommandHandler | undefined { | ||||||
|  |     return this.getHandlers().find((e) => { | ||||||
|  |       const hasGroupOrParent = e.group !== undefined || e.parent !== undefined | ||||||
|  |       const groupMatched = | ||||||
|  |         e.group !== undefined && e.parent !== undefined | ||||||
|  |           ? i.options | ||||||
|  |               .find( | ||||||
|  |                 (o) => | ||||||
|  |                   o.name === e.group && | ||||||
|  |                   o.type === SlashCommandOptionType.SUB_COMMAND_GROUP | ||||||
|  |               ) | ||||||
|  |               ?.options?.find((o) => o.name === e.name) !== undefined | ||||||
|  |           : true | ||||||
|  |       const subMatched = | ||||||
|  |         e.group === undefined && e.parent !== undefined | ||||||
|  |           ? i.options.find( | ||||||
|  |               (o) => | ||||||
|  |                 o.name === e.name && | ||||||
|  |                 o.type === SlashCommandOptionType.SUB_COMMAND | ||||||
|  |             ) !== undefined | ||||||
|  |           : true | ||||||
|  |       const nameMatched1 = e.name === i.name | ||||||
|  |       const parentMatched = hasGroupOrParent ? e.parent === i.name : true | ||||||
|  |       const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1 | ||||||
|  | 
 | ||||||
|  |       const matched = groupMatched && subMatched && nameMatched | ||||||
|  |       return matched | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Process an incoming Interaction */ | ||||||
|  |   private async _process(interaction: Interaction): Promise<void> { | ||||||
|  |     if (!this.enabled) return | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       interaction.type !== InteractionType.APPLICATION_COMMAND || | ||||||
|  |       interaction.data === undefined | ||||||
|  |     ) | ||||||
|  |       return | ||||||
|  | 
 | ||||||
|  |     const cmd = | ||||||
|  |       this._getCommand(interaction) ?? | ||||||
|  |       this.getHandlers().find((e) => e.name === '*') | ||||||
|  |     if (cmd?.group !== undefined) | ||||||
|  |       interaction.data.options = interaction.data.options[0].options ?? [] | ||||||
|  |     if (cmd?.parent !== undefined) | ||||||
|  |       interaction.data.options = interaction.data.options[0].options ?? [] | ||||||
|  | 
 | ||||||
|  |     if (cmd === undefined) return | ||||||
|  | 
 | ||||||
|  |     await this.emit('interaction', interaction) | ||||||
|  |     try { | ||||||
|  |       await cmd.handler(interaction) | ||||||
|  |     } catch (e) { | ||||||
|  |       await this.emit('interactionError', e) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Verify HTTP based Interaction */ | ||||||
|  |   async verifyKey( | ||||||
|  |     rawBody: string | Uint8Array, | ||||||
|  |     signature: string | Uint8Array, | ||||||
|  |     timestamp: string | Uint8Array | ||||||
|  |   ): Promise<boolean> { | ||||||
|  |     if (this.publicKey === undefined) | ||||||
|  |       throw new Error('Public Key is not present') | ||||||
|  | 
 | ||||||
|  |     const fullBody = new Uint8Array([ | ||||||
|  |       ...(typeof timestamp === 'string' ? encodeText(timestamp) : timestamp), | ||||||
|  |       ...(typeof rawBody === 'string' ? encodeText(rawBody) : rawBody) | ||||||
|  |     ]) | ||||||
|  | 
 | ||||||
|  |     return edverify(signature, fullBody, this.publicKey).catch(() => false) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction. **Data present in Interaction returned by this method is very different from actual typings as there is no real `Client` behind the scenes to cache things.** */ | ||||||
|  |   async verifyServerRequest(req: { | ||||||
|  |     headers: Headers | ||||||
|  |     method: string | ||||||
|  |     body: Deno.Reader | Uint8Array | ||||||
|  |     respond: (options: { | ||||||
|  |       status?: number | ||||||
|  |       headers?: Headers | ||||||
|  |       body?: any | ||||||
|  |     }) => Promise<void> | ||||||
|  |   }): Promise<false | Interaction> { | ||||||
|  |     if (req.method.toLowerCase() !== 'post') return false | ||||||
|  | 
 | ||||||
|  |     const signature = req.headers.get('x-signature-ed25519') | ||||||
|  |     const timestamp = req.headers.get('x-signature-timestamp') | ||||||
|  |     if (signature === null || timestamp === null) return false | ||||||
|  | 
 | ||||||
|  |     const rawbody = | ||||||
|  |       req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body) | ||||||
|  |     const verify = await this.verifyKey(rawbody, signature, timestamp) | ||||||
|  |     if (!verify) return false | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       const payload: InteractionPayload = JSON.parse(decodeText(rawbody)) | ||||||
|  | 
 | ||||||
|  |       // TODO: Maybe fix all this hackery going on here?
 | ||||||
|  |       const res = new Interaction(this as any, payload, { | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||||
|  |         user: new User(this as any, (payload.member?.user ?? payload.user)!), | ||||||
|  |         member: payload.member as any, | ||||||
|  |         guild: payload.guild_id as any, | ||||||
|  |         channel: payload.channel_id as any, | ||||||
|  |         resolved: ((payload.data | ||||||
|  |           ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { | ||||||
|  |           users: {}, | ||||||
|  |           members: {}, | ||||||
|  |           roles: {}, | ||||||
|  |           channels: {} | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       res._httpRespond = async (d: InteractionResponsePayload | FormData) => | ||||||
|  |         await req.respond({ | ||||||
|  |           status: 200, | ||||||
|  |           headers: new Headers({ | ||||||
|  |             'content-type': | ||||||
|  |               d instanceof FormData ? 'multipart/form-data' : 'application/json' | ||||||
|  |           }), | ||||||
|  |           body: d instanceof FormData ? d : JSON.stringify(d) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |       return res | ||||||
|  |     } catch (e) { | ||||||
|  |       return false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */ | ||||||
|  |   async verifyFetchEvent({ | ||||||
|  |     request: req, | ||||||
|  |     respondWith | ||||||
|  |   }: { | ||||||
|  |     respondWith: CallableFunction | ||||||
|  |     request: Request | ||||||
|  |   }): Promise<false | Interaction> { | ||||||
|  |     if (req.bodyUsed === true) throw new Error('Request Body already used') | ||||||
|  |     if (req.body === null) return false | ||||||
|  |     const body = (await req.body.getReader().read()).value | ||||||
|  |     if (body === undefined) return false | ||||||
|  | 
 | ||||||
|  |     return await this.verifyServerRequest({ | ||||||
|  |       headers: req.headers, | ||||||
|  |       body, | ||||||
|  |       method: req.method, | ||||||
|  |       respond: async (options) => { | ||||||
|  |         await respondWith( | ||||||
|  |           new Response(options.body, { | ||||||
|  |             headers: options.headers, | ||||||
|  |             status: options.status | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async verifyOpineRequest(req: any): 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: any, | ||||||
|  |     res: any, | ||||||
|  |     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: any): 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, signature, timestamp) | ||||||
|  |     if (!verified) return false | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Decorator to create a Slash Command handler */ | ||||||
|  | export function slash(name?: string, guild?: string) { | ||||||
|  |   return function (client: Client | SlashClient | SlashModule, prop: string) { | ||||||
|  |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|  |     const item = (client as { [name: string]: any })[prop] | ||||||
|  |     if (typeof item !== 'function') { | ||||||
|  |       throw new Error('@slash decorator requires a function') | ||||||
|  |     } else | ||||||
|  |       client._decoratedSlash.push({ | ||||||
|  |         name: name ?? prop, | ||||||
|  |         guild, | ||||||
|  |         handler: item | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Decorator to create a Sub-Slash Command handler */ | ||||||
|  | export function subslash(parent: string, name?: string, guild?: 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') { | ||||||
|  |       throw new Error('@subslash decorator requires a function') | ||||||
|  |     } else | ||||||
|  |       client._decoratedSlash.push({ | ||||||
|  |         parent, | ||||||
|  |         name: name ?? prop, | ||||||
|  |         guild, | ||||||
|  |         handler: item | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Decorator to create a Grouped Slash Command handler */ | ||||||
|  | export function groupslash( | ||||||
|  |   parent: string, | ||||||
|  |   group: string, | ||||||
|  |   name?: string, | ||||||
|  |   guild?: 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') { | ||||||
|  |       throw new Error('@groupslash decorator requires a function') | ||||||
|  |     } else | ||||||
|  |       client._decoratedSlash.push({ | ||||||
|  |         group, | ||||||
|  |         parent, | ||||||
|  |         name: name ?? prop, | ||||||
|  |         guild, | ||||||
|  |         handler: item | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import { Guild } from '../structures/guild.ts' | import { RESTManager } from '../rest/manager.ts' | ||||||
| import { Interaction } from '../structures/slash.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { | import { | ||||||
|   InteractionType, |  | ||||||
|   SlashCommandChoice, |   SlashCommandChoice, | ||||||
|   SlashCommandOption, |   SlashCommandOption, | ||||||
|   SlashCommandOptionType, |   SlashCommandOptionType, | ||||||
|  | @ -9,11 +8,7 @@ import { | ||||||
|   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 type { SlashClient, SlashCommandHandlerCallback } from './slashClient.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' |  | ||||||
| 
 | 
 | ||||||
| export class SlashCommand { | export class SlashCommand { | ||||||
|   slash: SlashCommandsManager |   slash: SlashCommandsManager | ||||||
|  | @ -155,6 +150,7 @@ function buildOptionsArray( | ||||||
|       ) |       ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Slash Command Builder */ | ||||||
| export class SlashBuilder { | export class SlashBuilder { | ||||||
|   data: SlashCommandPartial |   data: SlashCommandPartial | ||||||
| 
 | 
 | ||||||
|  | @ -200,6 +196,7 @@ export class SlashBuilder { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ | ||||||
| export class SlashCommandsManager { | export class SlashCommandsManager { | ||||||
|   slash: SlashClient |   slash: SlashClient | ||||||
|   rest: RESTManager |   rest: RESTManager | ||||||
|  | @ -350,229 +347,3 @@ export class SlashCommandsManager { | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export type SlashCommandHandlerCallback = (interaction: Interaction) => any |  | ||||||
| export interface SlashCommandHandler { |  | ||||||
|   name: string |  | ||||||
|   guild?: string |  | ||||||
|   parent?: string |  | ||||||
|   group?: string |  | ||||||
|   handler: SlashCommandHandlerCallback |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface SlashOptions { |  | ||||||
|   id?: string | (() => string) |  | ||||||
|   client?: Client |  | ||||||
|   enabled?: boolean |  | ||||||
|   token?: string |  | ||||||
|   rest?: RESTManager |  | ||||||
|   publicKey?: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class SlashClient { |  | ||||||
|   id: string | (() => string) |  | ||||||
|   client?: Client |  | ||||||
|   token?: string |  | ||||||
|   enabled: boolean = true |  | ||||||
|   commands: SlashCommandsManager |  | ||||||
|   handlers: SlashCommandHandler[] = [] |  | ||||||
|   rest: RESTManager |  | ||||||
|   modules: SlashModule[] = [] |  | ||||||
|   publicKey?: string |  | ||||||
| 
 |  | ||||||
|   _decoratedSlash?: Array<{ |  | ||||||
|     name: string |  | ||||||
|     guild?: string |  | ||||||
|     parent?: string |  | ||||||
|     group?: string |  | ||||||
|     handler: (interaction: Interaction) => any |  | ||||||
|   }> |  | ||||||
| 
 |  | ||||||
|   constructor(options: SlashOptions) { |  | ||||||
|     let id = options.id |  | ||||||
|     if (options.token !== undefined) id = atob(options.token?.split('.')[0]) |  | ||||||
|     if (id === undefined) |  | ||||||
|       throw new Error('ID could not be found. Pass at least client or token') |  | ||||||
|     this.id = id |  | ||||||
|     this.client = options.client |  | ||||||
|     this.token = options.token |  | ||||||
|     this.publicKey = options.publicKey |  | ||||||
| 
 |  | ||||||
|     this.enabled = options.enabled ?? true |  | ||||||
| 
 |  | ||||||
|     if (this.client?._decoratedSlash !== undefined) { |  | ||||||
|       this.client._decoratedSlash.forEach((e) => { |  | ||||||
|         e.handler = e.handler.bind(this.client) |  | ||||||
|         this.handlers.push(e) |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (this._decoratedSlash !== undefined) { |  | ||||||
|       this._decoratedSlash.forEach((e) => { |  | ||||||
|         e.handler = e.handler.bind(this.client) |  | ||||||
|         this.handlers.push(e) |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.rest = |  | ||||||
|       options.client === undefined |  | ||||||
|         ? options.rest === undefined |  | ||||||
|           ? new RESTManager({ |  | ||||||
|               token: this.token |  | ||||||
|             }) |  | ||||||
|           : options.rest |  | ||||||
|         : options.client.rest |  | ||||||
| 
 |  | ||||||
|     this.client?.on('interactionCreate', (interaction) => |  | ||||||
|       this._process(interaction) |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     this.commands = new SlashCommandsManager(this) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getID(): string { |  | ||||||
|     return typeof this.id === 'string' ? this.id : this.id() |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Adds a new Slash Command Handler */ |  | ||||||
|   handle(handler: SlashCommandHandler): SlashClient { |  | ||||||
|     this.handlers.push(handler) |  | ||||||
|     return this |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Load a Slash Module */ |  | ||||||
|   loadModule(module: SlashModule): SlashClient { |  | ||||||
|     this.modules.push(module) |  | ||||||
|     return this |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Get all Handlers. Including Slash Modules */ |  | ||||||
|   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 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */ |  | ||||||
|   private _getCommand(i: Interaction): SlashCommandHandler | undefined { |  | ||||||
|     return this.getHandlers().find((e) => { |  | ||||||
|       const hasGroupOrParent = e.group !== undefined || e.parent !== undefined |  | ||||||
|       const groupMatched = |  | ||||||
|         e.group !== undefined && e.parent !== undefined |  | ||||||
|           ? i.options |  | ||||||
|               .find((o) => o.name === e.group) |  | ||||||
|               ?.options?.find((o) => o.name === e.name) !== undefined |  | ||||||
|           : true |  | ||||||
|       const subMatched = |  | ||||||
|         e.group === undefined && e.parent !== undefined |  | ||||||
|           ? i.options.find((o) => o.name === e.name) !== undefined |  | ||||||
|           : true |  | ||||||
|       const nameMatched1 = e.name === i.name |  | ||||||
|       const parentMatched = hasGroupOrParent ? e.parent === i.name : true |  | ||||||
|       const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1 |  | ||||||
| 
 |  | ||||||
|       const matched = groupMatched && subMatched && nameMatched |  | ||||||
|       return matched |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Process an incoming Slash Command (interaction) */ |  | ||||||
|   private _process(interaction: Interaction): void { |  | ||||||
|     if (!this.enabled) return |  | ||||||
| 
 |  | ||||||
|     if (interaction.type !== InteractionType.APPLICATION_COMMAND) return |  | ||||||
| 
 |  | ||||||
|     const cmd = this._getCommand(interaction) |  | ||||||
|     if (cmd?.group !== undefined) |  | ||||||
|       interaction.data.options = interaction.data.options[0].options ?? [] |  | ||||||
|     if (cmd?.parent !== undefined) |  | ||||||
|       interaction.data.options = interaction.data.options[0].options ?? [] |  | ||||||
| 
 |  | ||||||
|     if (cmd === undefined) return |  | ||||||
| 
 |  | ||||||
|     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: any): 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: any, |  | ||||||
|     res: any, |  | ||||||
|     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: any): 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, signature, timestamp) |  | ||||||
|     if (!verified) return false |  | ||||||
|     return true |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { SlashCommandHandler } from './slashClient.ts' | import type { SlashCommandHandler } from './slashClient.ts' | ||||||
| 
 | 
 | ||||||
| export class SlashModule { | export class SlashModule { | ||||||
|   name: string = '' |   name: string = '' | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -63,7 +63,9 @@ export class BaseManager<T, T2> { | ||||||
|   async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { |   async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { | ||||||
|     const arr = (await this.array()) ?? [] |     const arr = (await this.array()) ?? [] | ||||||
|     const { readable, writable } = new TransformStream() |     const { readable, writable } = new TransformStream() | ||||||
|     arr.forEach((el) => writable.getWriter().write(el)) |     const writer = writable.getWriter() | ||||||
|  |     arr.forEach((el: unknown) => writer.write(el)) | ||||||
|  |     writer.close() | ||||||
|     yield* readable |     yield* readable | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
|  | @ -43,7 +43,9 @@ export class BaseChildManager<T, T2> { | ||||||
|   async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { |   async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { | ||||||
|     const arr = (await this.array()) ?? [] |     const arr = (await this.array()) ?? [] | ||||||
|     const { readable, writable } = new TransformStream() |     const { readable, writable } = new TransformStream() | ||||||
|     arr.forEach((el: unknown) => writable.getWriter().write(el)) |     const writer = writable.getWriter() | ||||||
|  |     arr.forEach((el: unknown) => writer.write(el)) | ||||||
|  |     writer.close() | ||||||
|     yield* readable |     yield* readable | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../client/mod.ts' | ||||||
| import { Channel } from '../structures/channel.ts' | import { Channel } from '../structures/channel.ts' | ||||||
| import { Embed } from '../structures/embed.ts' | import { Embed } from '../structures/embed.ts' | ||||||
| import { Message } from '../structures/message.ts' | import { Message } from '../structures/message.ts' | ||||||
| import { TextChannel } from '../structures/textChannel.ts' | import type { TextChannel } from '../structures/textChannel.ts' | ||||||
| import { | import type { | ||||||
|   ChannelPayload, |   ChannelPayload, | ||||||
|   GuildChannelPayload, |   GuildChannelPayload, | ||||||
|   MessageOptions |   MessageOptions | ||||||
| } from '../types/channel.ts' | } from '../types/channel.ts' | ||||||
| import { CHANNEL } from '../types/endpoint.ts' | import { CHANNEL } from '../types/endpoint.ts' | ||||||
| import getChannelByType from '../utils/getChannelByType.ts' | import getChannelByType from '../utils/channel.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export type AllMessageOptions = MessageOptions | Embed | export type AllMessageOptions = MessageOptions | Embed | ||||||
|  | @ -121,6 +121,10 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> { | ||||||
|           : undefined |           : undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (payload.content === undefined && payload.embed === undefined) { | ||||||
|  |       payload.content = '' | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const resp = await this.client.rest.api.channels[channelID].messages.post( |     const resp = await this.client.rest.api.channels[channelID].messages.post( | ||||||
|       payload |       payload | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Emoji } from '../structures/emoji.ts' | import { Emoji } from '../structures/emoji.ts' | ||||||
| import { EmojiPayload } from '../types/emoji.ts' | import type { EmojiPayload } from '../types/emoji.ts' | ||||||
| import { GUILD_EMOJI } from '../types/endpoint.ts' | import { GUILD_EMOJI } from '../types/endpoint.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Cache Manager used for Caching values related to Gateway connection |  * Cache Manager used for Caching values related to Gateway connection | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { BaseChildManager } from './baseChild.ts' | import { BaseChildManager } from './baseChild.ts' | ||||||
| import { VoiceStatePayload } from '../types/voice.ts' | import type { VoiceStatePayload } from '../types/voice.ts' | ||||||
| import { VoiceState } from '../structures/voiceState.ts' | import { VoiceState } from '../structures/voiceState.ts' | ||||||
| import { GuildVoiceStatesManager } from './guildVoiceStates.ts' | import { GuildVoiceStatesManager } from './guildVoiceStates.ts' | ||||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
| 
 | 
 | ||||||
| export class GuildChannelVoiceStatesManager extends BaseChildManager< | export class GuildChannelVoiceStatesManager extends BaseChildManager< | ||||||
|   VoiceStatePayload, |   VoiceStatePayload, | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Channel } from '../structures/channel.ts' | import { Channel } from '../structures/channel.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import { Guild } from '../structures/guild.ts' | ||||||
| import { CategoryChannel } from '../structures/guildCategoryChannel.ts' | import type { CategoryChannel } from '../structures/guildCategoryChannel.ts' | ||||||
| import { | import { | ||||||
|   ChannelTypes, |   ChannelTypes, | ||||||
|   GuildChannelPayload, |   GuildChannelPayload, | ||||||
|   OverwritePayload |   OverwritePayload | ||||||
| } from '../types/channel.ts' | } from '../types/channel.ts' | ||||||
| import { GuildChannels, GuildChannelPayloads } from '../types/guild.ts' | import type { GuildChannels, GuildChannelPayloads } from '../types/guild.ts' | ||||||
| import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts' | import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts' | ||||||
| import { BaseChildManager } from './baseChild.ts' | import { BaseChildManager } from './baseChild.ts' | ||||||
| import { ChannelsManager } from './channels.ts' | import type { ChannelsManager } from './channels.ts' | ||||||
| 
 | 
 | ||||||
| export interface CreateChannelOptions { | export interface CreateChannelOptions { | ||||||
|   name: string |   name: string | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Emoji } from '../structures/emoji.ts' | import { Emoji } from '../structures/emoji.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Role } from '../structures/role.ts' | import { Role } from '../structures/role.ts' | ||||||
| import { EmojiPayload } from '../types/emoji.ts' | import type { EmojiPayload } from '../types/emoji.ts' | ||||||
| import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts' | import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts' | ||||||
| import { BaseChildManager } from './baseChild.ts' | import { BaseChildManager } from './baseChild.ts' | ||||||
| import { EmojisManager } from './emojis.ts' | import type { EmojisManager } from './emojis.ts' | ||||||
| import { fetchAuto } from '../../deps.ts' | import { fetchAuto } from '../../deps.ts' | ||||||
| 
 | 
 | ||||||
| export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> { | export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> { | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
| import { User } from '../structures/user.ts' | import type { User } from '../structures/user.ts' | ||||||
| import { VoiceState } from '../structures/voiceState.ts' | import { VoiceState } from '../structures/voiceState.ts' | ||||||
| import { VoiceStatePayload } from '../types/voice.ts' | import type { VoiceStatePayload } from '../types/voice.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export class GuildVoiceStatesManager extends BaseManager< | export class GuildVoiceStatesManager extends BaseManager< | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { fetchAuto } from '../../deps.ts' | import { fetchAuto } from '../../deps.ts' | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import { Guild } from '../structures/guild.ts' | ||||||
| import { Template } from '../structures/template.ts' | import type { Template } from '../structures/template.ts' | ||||||
| import { Role } from '../structures/role.ts' | import { Role } from '../structures/role.ts' | ||||||
| import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts' | import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts' | ||||||
| import { | import type { | ||||||
|   GuildPayload, |   GuildPayload, | ||||||
|   MemberPayload, |   MemberPayload, | ||||||
|   GuildCreateRolePayload, |   GuildCreateRolePayload, | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { GuildTextChannel, User } from '../../mod.ts' | import type { GuildTextChannel, User } from '../../mod.ts' | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Invite } from '../structures/invite.ts' | import { Invite } from '../structures/invite.ts' | ||||||
| import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts' | import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts' | ||||||
| import { InvitePayload } from '../types/invite.ts' | import type { InvitePayload } from '../types/invite.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export enum InviteTargetUserType { | export enum InviteTargetUserType { | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { BaseChildManager } from './baseChild.ts' | import { BaseChildManager } from './baseChild.ts' | ||||||
| import { RolePayload } from '../types/role.ts' | import type { RolePayload } from '../types/role.ts' | ||||||
| import { Role } from '../structures/role.ts' | import { Role } from '../structures/role.ts' | ||||||
| import { Member } from '../structures/member.ts' | import type { Member } from '../structures/member.ts' | ||||||
| import { RolesManager } from './roles.ts' | import type { RolesManager } from './roles.ts' | ||||||
| import { MemberPayload } from '../types/guild.ts' | import type { MemberPayload } from '../types/guild.ts' | ||||||
| import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts' | import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts' | ||||||
| 
 | 
 | ||||||
| export class MemberRolesManager extends BaseChildManager<RolePayload, Role> { | export class MemberRolesManager extends BaseChildManager<RolePayload, Role> { | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { User } from '../structures/user.ts' | import { User } from '../structures/user.ts' | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Member } from '../structures/member.ts' | import { Member } from '../structures/member.ts' | ||||||
| import { GUILD_MEMBER } from '../types/endpoint.ts' | import { GUILD_MEMBER } from '../types/endpoint.ts' | ||||||
| import { MemberPayload } from '../types/guild.ts' | import type { MemberPayload } from '../types/guild.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| import { Permissions } from '../utils/permissions.ts' | import { Permissions } from '../utils/permissions.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,10 +1,9 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Emoji } from '../structures/emoji.ts' | import { Emoji } from '../structures/emoji.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Message } from '../structures/message.ts' | ||||||
| import { Message } from '../structures/message.ts' |  | ||||||
| import { MessageReaction } from '../structures/messageReaction.ts' | import { MessageReaction } from '../structures/messageReaction.ts' | ||||||
| import { User } from '../structures/user.ts' | import type { User } from '../structures/user.ts' | ||||||
| import { Reaction } from '../types/channel.ts' | import type { Reaction } from '../types/channel.ts' | ||||||
| import { | import { | ||||||
|   MESSAGE_REACTION, |   MESSAGE_REACTION, | ||||||
|   MESSAGE_REACTIONS, |   MESSAGE_REACTIONS, | ||||||
|  | @ -19,7 +18,7 @@ export class MessageReactionsManager extends BaseManager< | ||||||
|   message: Message |   message: Message | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, message: Message) { |   constructor(client: Client, message: Message) { | ||||||
|     super(client, `reactions:${message.id}`, Guild) |     super(client, `reactions:${message.id}`, MessageReaction) | ||||||
|     this.message = message |     this.message = message | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Message } from '../structures/message.ts' | import { Message } from '../structures/message.ts' | ||||||
| import { TextChannel } from '../structures/textChannel.ts' | import type { TextChannel } from '../structures/textChannel.ts' | ||||||
| import { User } from '../structures/user.ts' | import { User } from '../structures/user.ts' | ||||||
| import { MessagePayload } from '../types/channel.ts' | import type { MessagePayload } from '../types/channel.ts' | ||||||
| import { CHANNEL_MESSAGE } from '../types/endpoint.ts' | import { CHANNEL_MESSAGE } from '../types/endpoint.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Presence } from '../structures/presence.ts' | import { Presence } from '../structures/presence.ts' | ||||||
| import { User } from '../structures/user.ts' | import { User } from '../structures/user.ts' | ||||||
| import { PresenceUpdatePayload } from '../types/gateway.ts' | import type { PresenceUpdatePayload } from '../types/gateway.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export class GuildPresencesManager extends BaseManager< | export class GuildPresencesManager extends BaseManager< | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { MessageReaction } from '../structures/messageReaction.ts' | import type { MessageReaction } from '../structures/messageReaction.ts' | ||||||
| import { User } from '../structures/user.ts' | import type { User } from '../structures/user.ts' | ||||||
| import { UsersManager } from './users.ts' | import { UsersManager } from './users.ts' | ||||||
| 
 | 
 | ||||||
| export class ReactionUsersManager extends UsersManager { | export class ReactionUsersManager extends UsersManager { | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { Permissions } from '../../mod.ts' | import { Permissions } from '../../mod.ts' | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import type { Guild } from '../structures/guild.ts' | ||||||
| import { Role } from '../structures/role.ts' | import { Role } from '../structures/role.ts' | ||||||
| import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts' | import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts' | ||||||
| import { RoleModifyPayload, RolePayload } from '../types/role.ts' | import type { RoleModifyPayload, RolePayload } from '../types/role.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export interface CreateGuildRoleOptions { | export interface CreateGuildRoleOptions { | ||||||
|  | @ -22,14 +22,17 @@ export class RolesManager extends BaseManager<RolePayload, Role> { | ||||||
|     this.guild = guild |     this.guild = guild | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Fetch a Guild Role (from API) */ |   /** Fetch All Guild Roles */ | ||||||
|   async fetch(id: string): Promise<Role> { |   async fetchAll(): Promise<Role[]> { | ||||||
|     return await new Promise((resolve, reject) => { |     return await new Promise((resolve, reject) => { | ||||||
|       this.client.rest |       this.client.rest.api.guilds[this.guild.id].roles.get | ||||||
|         .get(GUILD_ROLE(this.guild.id, id)) |         .then(async (data: RolePayload[]) => { | ||||||
|         .then(async (data) => { |           const roles: Role[] = [] | ||||||
|           await this.set(id, data as RolePayload) |           for (const raw of data) { | ||||||
|           resolve(((await this.get(id)) as unknown) as Role) |             await this.set(raw.id, raw) | ||||||
|  |             roles.push(new Role(this.client, raw, this.guild)) | ||||||
|  |           } | ||||||
|  |           resolve(roles) | ||||||
|         }) |         }) | ||||||
|         .catch((e) => reject(e)) |         .catch((e) => reject(e)) | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { User } from '../structures/user.ts' | import { User } from '../structures/user.ts' | ||||||
| import { USER } from '../types/endpoint.ts' | import { USER } from '../types/endpoint.ts' | ||||||
| import { UserPayload } from '../types/user.ts' | import type { UserPayload } from '../types/user.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export class UsersManager extends BaseManager<UserPayload, User> { | export class UsersManager extends BaseManager<UserPayload, User> { | ||||||
|  |  | ||||||
|  | @ -1,676 +0,0 @@ | ||||||
| import * as baseEndpoints from '../consts/urlsAndVersions.ts' |  | ||||||
| import { Embed } from '../structures/embed.ts' |  | ||||||
| import { MessageAttachment } from '../structures/message.ts' |  | ||||||
| import { Collection } from '../utils/collection.ts' |  | ||||||
| import { Client } from './client.ts' |  | ||||||
| import { simplifyAPIError } from '../utils/err_fmt.ts' |  | ||||||
| 
 |  | ||||||
| export type RequestMethods = |  | ||||||
|   | 'get' |  | ||||||
|   | 'post' |  | ||||||
|   | 'put' |  | ||||||
|   | 'patch' |  | ||||||
|   | 'head' |  | ||||||
|   | 'delete' |  | ||||||
| 
 |  | ||||||
| export enum HttpResponseCode { |  | ||||||
|   Ok = 200, |  | ||||||
|   Created = 201, |  | ||||||
|   NoContent = 204, |  | ||||||
|   NotModified = 304, |  | ||||||
|   BadRequest = 400, |  | ||||||
|   Unauthorized = 401, |  | ||||||
|   Forbidden = 403, |  | ||||||
|   NotFound = 404, |  | ||||||
|   MethodNotAllowed = 405, |  | ||||||
|   TooManyRequests = 429, |  | ||||||
|   GatewayUnavailable = 502 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface RequestHeaders { |  | ||||||
|   [name: string]: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface DiscordAPIErrorPayload { |  | ||||||
|   url: string |  | ||||||
|   status: number |  | ||||||
|   method: string |  | ||||||
|   code?: number |  | ||||||
|   message?: string |  | ||||||
|   errors: object |  | ||||||
|   requestData: { [key: string]: any } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export class DiscordAPIError extends Error { |  | ||||||
|   name = 'DiscordAPIError' |  | ||||||
|   error?: DiscordAPIErrorPayload |  | ||||||
| 
 |  | ||||||
|   constructor(error: string | DiscordAPIErrorPayload) { |  | ||||||
|     super() |  | ||||||
|     const fmt = Object.entries( |  | ||||||
|       typeof error === 'object' ? simplifyAPIError(error.errors) : {} |  | ||||||
|     ) |  | ||||||
|     this.message = |  | ||||||
|       typeof error === 'string' |  | ||||||
|         ? `${error} ` |  | ||||||
|         : `\n${error.method} ${error.url.slice(7)} returned ${error.status}\n(${ |  | ||||||
|             error.code ?? 'unknown' |  | ||||||
|           }) ${error.message}${ |  | ||||||
|             fmt.length === 0 |  | ||||||
|               ? '' |  | ||||||
|               : `\n${fmt |  | ||||||
|                   .map( |  | ||||||
|                     (e) => |  | ||||||
|                       `  at ${e[0]}:\n${e[1] |  | ||||||
|                         .map((e) => `   - ${e}`) |  | ||||||
|                         .join('\n')}` |  | ||||||
|                   ) |  | ||||||
|                   .join('\n')}\n` |  | ||||||
|           }` |  | ||||||
|     if (typeof error === 'object') this.error = error |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface QueuedItem { |  | ||||||
|   bucket?: string | null |  | ||||||
|   url: string |  | ||||||
|   onComplete: () => Promise< |  | ||||||
|     | { |  | ||||||
|         rateLimited: any |  | ||||||
|         bucket?: string | null |  | ||||||
|         before: boolean |  | ||||||
|       } |  | ||||||
|     | undefined |  | ||||||
|   > |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface RateLimit { |  | ||||||
|   url: string |  | ||||||
|   resetAt: number |  | ||||||
|   bucket: string | null |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head'] |  | ||||||
| 
 |  | ||||||
| export type MethodFunction = ( |  | ||||||
|   body?: unknown, |  | ||||||
|   maxRetries?: number, |  | ||||||
|   bucket?: string | null, |  | ||||||
|   rawResponse?: boolean |  | ||||||
| ) => Promise<any> |  | ||||||
| 
 |  | ||||||
| export interface APIMap extends MethodFunction { |  | ||||||
|   /** Make a GET request to current route */ |  | ||||||
|   get: APIMap |  | ||||||
|   /** Make a POST request to current route */ |  | ||||||
|   post: APIMap |  | ||||||
|   /** Make a PATCH request to current route */ |  | ||||||
|   patch: APIMap |  | ||||||
|   /** Make a PUT request to current route */ |  | ||||||
|   put: APIMap |  | ||||||
|   /** Make a DELETE request to current route */ |  | ||||||
|   delete: APIMap |  | ||||||
|   /** Make a HEAD request to current route */ |  | ||||||
|   head: APIMap |  | ||||||
|   /** Continue building API Route */ |  | ||||||
|   [name: string]: APIMap |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** API Route builder function */ |  | ||||||
| export const builder = (rest: RESTManager, acum = '/'): APIMap => { |  | ||||||
|   const routes = {} |  | ||||||
|   const proxy = new Proxy(routes, { |  | ||||||
|     get: (_, p, __) => { |  | ||||||
|       if (p === 'toString') return () => acum |  | ||||||
|       if (METHODS.includes(String(p))) { |  | ||||||
|         const method = ((rest as unknown) as { |  | ||||||
|           [name: string]: MethodFunction |  | ||||||
|         })[String(p)] |  | ||||||
|         return async (...args: any[]) => |  | ||||||
|           await method.bind(rest)( |  | ||||||
|             `${baseEndpoints.DISCORD_API_URL}/v${rest.version}${acum.substring( |  | ||||||
|               0, |  | ||||||
|               acum.length - 1 |  | ||||||
|             )}`,
 |  | ||||||
|             ...args |  | ||||||
|           ) |  | ||||||
|       } |  | ||||||
|       return builder(rest, acum + String(p) + '/') |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   return (proxy as unknown) as APIMap |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface RESTOptions { |  | ||||||
|   /** Token to use for authorization */ |  | ||||||
|   token?: string | (() => string | undefined) |  | ||||||
|   /** Headers to patch with if any */ |  | ||||||
|   headers?: { [name: string]: string | undefined } |  | ||||||
|   /** Whether to use Canary instance of Discord API or not */ |  | ||||||
|   canary?: boolean |  | ||||||
|   /** Discord REST API version to use */ |  | ||||||
|   version?: 6 | 7 | 8 |  | ||||||
|   /** Token Type to use for Authorization */ |  | ||||||
|   tokenType?: TokenType |  | ||||||
|   /** User Agent to use (Header) */ |  | ||||||
|   userAgent?: string |  | ||||||
|   /** Optional Harmony client */ |  | ||||||
|   client?: Client |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** Token Type for REST API. */ |  | ||||||
| export enum TokenType { |  | ||||||
|   /** Token type for Bot User */ |  | ||||||
|   Bot = 'Bot', |  | ||||||
|   /** Token Type for OAuth2 */ |  | ||||||
|   Bearer = 'Bearer', |  | ||||||
|   /** No Token Type. Can be used for User accounts. */ |  | ||||||
|   None = '' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** An easier to use interface for interacting with Discord REST API. */ |  | ||||||
| export class RESTManager { |  | ||||||
|   queues: { [key: string]: QueuedItem[] } = {} |  | ||||||
|   rateLimits = new Collection<string, RateLimit>() |  | ||||||
|   /** Whether we are globally ratelimited or not */ |  | ||||||
|   globalRateLimit: boolean = false |  | ||||||
|   /** Whether requests are being processed or not */ |  | ||||||
|   processing: boolean = false |  | ||||||
|   /** API Version being used by REST Manager */ |  | ||||||
|   version: number = 8 |  | ||||||
|   /** |  | ||||||
|    * API Map - easy to use way for interacting with Discord API. |  | ||||||
|    * |  | ||||||
|    * Examples: |  | ||||||
|    * * ```ts
 |  | ||||||
|    *   rest.api.users['123'].get().then(userPayload => doSomething) |  | ||||||
|    *   ``` |  | ||||||
|    * * ```ts
 |  | ||||||
|    *   rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {}) |  | ||||||
|    *   ``` |  | ||||||
|    */ |  | ||||||
|   api: APIMap |  | ||||||
|   /** Token being used for Authorization */ |  | ||||||
|   token?: string | (() => string | undefined) |  | ||||||
|   /** Token Type of the Token if any */ |  | ||||||
|   tokenType: TokenType = TokenType.Bot |  | ||||||
|   /** Headers object which patch the current ones */ |  | ||||||
|   headers: any = {} |  | ||||||
|   /** Optional custom User Agent (header) */ |  | ||||||
|   userAgent?: string |  | ||||||
|   /** Whether REST Manager is using Canary API */ |  | ||||||
|   canary?: boolean |  | ||||||
|   /** Optional Harmony Client object */ |  | ||||||
|   client?: Client |  | ||||||
| 
 |  | ||||||
|   constructor(options?: RESTOptions) { |  | ||||||
|     this.api = builder(this) |  | ||||||
|     if (options?.token !== undefined) this.token = options.token |  | ||||||
|     if (options?.version !== undefined) this.version = options.version |  | ||||||
|     if (options?.headers !== undefined) this.headers = options.headers |  | ||||||
|     if (options?.tokenType !== undefined) this.tokenType = options.tokenType |  | ||||||
|     if (options?.userAgent !== undefined) this.userAgent = options.userAgent |  | ||||||
|     if (options?.canary !== undefined) this.canary = options.canary |  | ||||||
|     if (options?.client !== undefined) this.client = options.client |  | ||||||
|     this.handleRateLimits() |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Checks the queues of buckets, if empty, delete entry */ |  | ||||||
|   private checkQueues(): void { |  | ||||||
|     Object.entries(this.queues).forEach(([key, value]) => { |  | ||||||
|       if (value.length === 0) { |  | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 |  | ||||||
|         delete this.queues[key] |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Adds a Request to Queue */ |  | ||||||
|   private queue(request: QueuedItem): void { |  | ||||||
|     const route = request.url.substring( |  | ||||||
|       Number(baseEndpoints.DISCORD_API_URL.length) + 1 |  | ||||||
|     ) |  | ||||||
|     const parts = route.split('/') |  | ||||||
|     parts.shift() |  | ||||||
|     const [id] = parts |  | ||||||
| 
 |  | ||||||
|     if (this.queues[id] !== undefined) { |  | ||||||
|       this.queues[id].push(request) |  | ||||||
|     } else { |  | ||||||
|       this.queues[id] = [request] |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private async processQueue(): Promise<void> { |  | ||||||
|     if (Object.keys(this.queues).length !== 0 && !this.globalRateLimit) { |  | ||||||
|       await Promise.allSettled( |  | ||||||
|         Object.values(this.queues).map(async (pathQueue) => { |  | ||||||
|           const request = pathQueue.shift() |  | ||||||
|           if (request === undefined) return |  | ||||||
| 
 |  | ||||||
|           const rateLimitedURLResetIn = await this.isRateLimited(request.url) |  | ||||||
| 
 |  | ||||||
|           if (typeof request.bucket === 'string') { |  | ||||||
|             const rateLimitResetIn = await this.isRateLimited(request.bucket) |  | ||||||
|             if (rateLimitResetIn !== false) { |  | ||||||
|               this.queue(request) |  | ||||||
|             } else { |  | ||||||
|               const result = await request.onComplete() |  | ||||||
|               if (result?.rateLimited !== undefined) { |  | ||||||
|                 this.queue({ |  | ||||||
|                   ...request, |  | ||||||
|                   bucket: result.bucket ?? request.bucket |  | ||||||
|                 }) |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } else { |  | ||||||
|             if (rateLimitedURLResetIn !== false) { |  | ||||||
|               this.queue(request) |  | ||||||
|             } else { |  | ||||||
|               const result = await request.onComplete() |  | ||||||
|               if (result?.rateLimited !== undefined) { |  | ||||||
|                 this.queue({ |  | ||||||
|                   ...request, |  | ||||||
|                   bucket: result.bucket ?? request.bucket |  | ||||||
|                 }) |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (Object.keys(this.queues).length !== 0) { |  | ||||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |  | ||||||
|       this.processQueue() |  | ||||||
|       this.checkQueues() |  | ||||||
|     } else this.processing = false |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private prepare(body: any, method: RequestMethods): { [key: string]: any } { |  | ||||||
|     const headers: RequestHeaders = { |  | ||||||
|       'User-Agent': |  | ||||||
|         this.userAgent ?? |  | ||||||
|         `DiscordBot (harmony, https://github.com/harmonyland/harmony)` |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (this.token !== undefined) { |  | ||||||
|       const token = typeof this.token === 'string' ? this.token : this.token() |  | ||||||
|       if (token !== undefined) |  | ||||||
|         headers.Authorization = `${this.tokenType} ${token}`.trim() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (method === 'get' || method === 'head' || method === 'delete') |  | ||||||
|       body = undefined |  | ||||||
| 
 |  | ||||||
|     if (body?.reason !== undefined) { |  | ||||||
|       headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let _files: undefined | MessageAttachment[] |  | ||||||
|     if (body?.embed?.files !== undefined && Array.isArray(body?.embed?.files)) { |  | ||||||
|       _files = body?.embed?.files |  | ||||||
|     } |  | ||||||
|     if (body?.embeds !== undefined && Array.isArray(body?.embeds)) { |  | ||||||
|       const files1 = body?.embeds |  | ||||||
|         .map((e: Embed) => e.files) |  | ||||||
|         .filter((e: MessageAttachment[]) => e !== undefined) |  | ||||||
|       for (const files of files1) { |  | ||||||
|         for (const file of files) { |  | ||||||
|           if (_files === undefined) _files = [] |  | ||||||
|           _files?.push(file) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if ( |  | ||||||
|       body?.file !== undefined || |  | ||||||
|       body?.files !== undefined || |  | ||||||
|       _files !== undefined |  | ||||||
|     ) { |  | ||||||
|       const files: Array<{ blob: Blob; name: string }> = [] |  | ||||||
|       if (body?.file !== undefined) files.push(body.file) |  | ||||||
|       if (body?.files !== undefined && Array.isArray(body.files)) { |  | ||||||
|         for (const file of body.files) { |  | ||||||
|           files.push(file) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (_files !== undefined) { |  | ||||||
|         for (const file of _files) { |  | ||||||
|           files.push(file) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       const form = new FormData() |  | ||||||
|       files.forEach((file, index) => |  | ||||||
|         form.append(`file${index + 1}`, file.blob, file.name) |  | ||||||
|       ) |  | ||||||
|       const json = JSON.stringify(body) |  | ||||||
|       form.append('payload_json', json) |  | ||||||
|       if (body === undefined) body = {} |  | ||||||
|       body.file = form |  | ||||||
|     } else if ( |  | ||||||
|       body !== undefined && |  | ||||||
|       !['get', 'delete'].includes(method.toLowerCase()) |  | ||||||
|     ) { |  | ||||||
|       headers['Content-Type'] = 'application/json' |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (this.headers !== undefined) Object.assign(headers, this.headers) |  | ||||||
|     const data: { [name: string]: any } = { |  | ||||||
|       headers, |  | ||||||
|       body: body?.file ?? JSON.stringify(body), |  | ||||||
|       method: method.toUpperCase() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return data |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private isRateLimited(url: string): number | false { |  | ||||||
|     const global = this.rateLimits.get('global') |  | ||||||
|     const rateLimited = this.rateLimits.get(url) |  | ||||||
|     const now = Date.now() |  | ||||||
| 
 |  | ||||||
|     if (rateLimited !== undefined && now < rateLimited.resetAt) { |  | ||||||
|       return rateLimited.resetAt - now |  | ||||||
|     } |  | ||||||
|     if (global !== undefined && now < global.resetAt) { |  | ||||||
|       return global.resetAt - now |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return false |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Processes headers of the Response */ |  | ||||||
|   private processHeaders( |  | ||||||
|     url: string, |  | ||||||
|     headers: Headers |  | ||||||
|   ): string | null | undefined { |  | ||||||
|     let rateLimited = false |  | ||||||
| 
 |  | ||||||
|     const global = headers.get('x-ratelimit-global') |  | ||||||
|     const bucket = headers.get('x-ratelimit-bucket') |  | ||||||
|     const remaining = headers.get('x-ratelimit-remaining') |  | ||||||
|     const resetAt = headers.get('x-ratelimit-reset') |  | ||||||
|     const retryAfter = headers.get('retry-after') |  | ||||||
| 
 |  | ||||||
|     if (remaining !== null && remaining === '0') { |  | ||||||
|       rateLimited = true |  | ||||||
| 
 |  | ||||||
|       this.rateLimits.set(url, { |  | ||||||
|         url, |  | ||||||
|         resetAt: Number(resetAt) * 1000, |  | ||||||
|         bucket |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       if (bucket !== null) { |  | ||||||
|         this.rateLimits.set(bucket, { |  | ||||||
|           url, |  | ||||||
|           resetAt: Number(resetAt) * 1000, |  | ||||||
|           bucket |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (global !== null) { |  | ||||||
|       const reset = Date.now() + Number(retryAfter) |  | ||||||
|       this.globalRateLimit = true |  | ||||||
|       rateLimited = true |  | ||||||
| 
 |  | ||||||
|       this.rateLimits.set('global', { |  | ||||||
|         url: 'global', |  | ||||||
|         resetAt: reset, |  | ||||||
|         bucket |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       if (bucket !== null) { |  | ||||||
|         this.rateLimits.set(bucket, { |  | ||||||
|           url: 'global', |  | ||||||
|           resetAt: reset, |  | ||||||
|           bucket |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return rateLimited ? bucket : undefined |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Handles status code of response and acts as required */ |  | ||||||
|   private handleStatusCode( |  | ||||||
|     response: Response, |  | ||||||
|     body: any, |  | ||||||
|     data: { [key: string]: any }, |  | ||||||
|     reject: CallableFunction |  | ||||||
|   ): void { |  | ||||||
|     const status = response.status |  | ||||||
| 
 |  | ||||||
|     // We have hit ratelimit - this should not happen
 |  | ||||||
|     if (status === HttpResponseCode.TooManyRequests) { |  | ||||||
|       if (this.client !== undefined) |  | ||||||
|         this.client.emit('rateLimit', { |  | ||||||
|           method: data.method, |  | ||||||
|           url: response.url, |  | ||||||
|           body |  | ||||||
|         }) |  | ||||||
|       reject(new Error('RateLimited')) |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // It's a normal status code... just continue
 |  | ||||||
|     if ( |  | ||||||
|       (status >= 200 && status < 400) || |  | ||||||
|       status === HttpResponseCode.NoContent |  | ||||||
|     ) |  | ||||||
|       return |  | ||||||
| 
 |  | ||||||
|     let text: undefined | string = Deno.inspect( |  | ||||||
|       body.errors === undefined ? body : body.errors |  | ||||||
|     ) |  | ||||||
|     if (text === 'undefined') text = undefined |  | ||||||
| 
 |  | ||||||
|     if (status === HttpResponseCode.Unauthorized) |  | ||||||
|       reject( |  | ||||||
|         new DiscordAPIError(`Request was Unauthorized. Invalid Token.\n${text}`) |  | ||||||
|       ) |  | ||||||
| 
 |  | ||||||
|     const _data = { ...data } |  | ||||||
|     if (_data?.headers !== undefined) delete _data.headers |  | ||||||
|     if (_data?.method !== undefined) delete _data.method |  | ||||||
| 
 |  | ||||||
|     // At this point we know it is error
 |  | ||||||
|     const error: DiscordAPIErrorPayload = { |  | ||||||
|       url: new URL(response.url).pathname, |  | ||||||
|       status, |  | ||||||
|       method: data.method, |  | ||||||
|       code: body?.code, |  | ||||||
|       message: body?.message, |  | ||||||
|       errors: body?.errors ?? {}, |  | ||||||
|       requestData: _data |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if ( |  | ||||||
|       [ |  | ||||||
|         HttpResponseCode.BadRequest, |  | ||||||
|         HttpResponseCode.NotFound, |  | ||||||
|         HttpResponseCode.Forbidden, |  | ||||||
|         HttpResponseCode.MethodNotAllowed |  | ||||||
|       ].includes(status) |  | ||||||
|     ) { |  | ||||||
|       reject(new DiscordAPIError(error)) |  | ||||||
|     } else if (status === HttpResponseCode.GatewayUnavailable) { |  | ||||||
|       reject(new DiscordAPIError(error)) |  | ||||||
|     } else reject(new DiscordAPIError('Request - Unknown Error')) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Makes a Request to Discord API. |  | ||||||
|    * @param method HTTP Method to use |  | ||||||
|    * @param url URL of the Request |  | ||||||
|    * @param body Body to send with Request |  | ||||||
|    * @param maxRetries Number of Max Retries to perform |  | ||||||
|    * @param bucket BucketID of the Request |  | ||||||
|    * @param rawResponse Whether to get Raw Response or body itself |  | ||||||
|    */ |  | ||||||
|   async make( |  | ||||||
|     method: RequestMethods, |  | ||||||
|     url: string, |  | ||||||
|     body?: unknown, |  | ||||||
|     maxRetries = 0, |  | ||||||
|     bucket?: string | null, |  | ||||||
|     rawResponse?: boolean |  | ||||||
|   ): Promise<any> { |  | ||||||
|     return await new Promise((resolve, reject) => { |  | ||||||
|       const onComplete = async (): Promise<undefined | any> => { |  | ||||||
|         try { |  | ||||||
|           const rateLimitResetIn = await this.isRateLimited(url) |  | ||||||
|           if (rateLimitResetIn !== false) { |  | ||||||
|             return { |  | ||||||
|               rateLimited: rateLimitResetIn, |  | ||||||
|               before: true, |  | ||||||
|               bucket |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           const query = |  | ||||||
|             method === 'get' && body !== undefined |  | ||||||
|               ? Object.entries(body as any) |  | ||||||
|                   .filter(([k, v]) => v !== undefined) |  | ||||||
|                   .map( |  | ||||||
|                     ([key, value]) => |  | ||||||
|                       `${encodeURIComponent(key)}=${encodeURIComponent( |  | ||||||
|                         value as any |  | ||||||
|                       )}` |  | ||||||
|                   ) |  | ||||||
|                   .join('&') |  | ||||||
|               : '' |  | ||||||
|           let urlToUse = |  | ||||||
|             method === 'get' && query !== '' ? `${url}?${query}` : url |  | ||||||
| 
 |  | ||||||
|           // It doesn't start with HTTP, that means it's an incomplete URL
 |  | ||||||
|           if (!urlToUse.startsWith('http')) { |  | ||||||
|             if (!urlToUse.startsWith('/')) urlToUse = `/${urlToUse}` |  | ||||||
|             urlToUse = |  | ||||||
|               // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
 |  | ||||||
|               baseEndpoints.DISCORD_API_URL + |  | ||||||
|               '/v' + |  | ||||||
|               baseEndpoints.DISCORD_API_VERSION + |  | ||||||
|               urlToUse |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           if (this.canary === true && urlToUse.startsWith('http')) { |  | ||||||
|             const split = urlToUse.split('//') |  | ||||||
|             urlToUse = split[0] + '//canary.' + split[1] |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           const requestData = this.prepare(body, method) |  | ||||||
| 
 |  | ||||||
|           const response = await fetch(urlToUse, requestData) |  | ||||||
|           const bucketFromHeaders = this.processHeaders(url, response.headers) |  | ||||||
| 
 |  | ||||||
|           if (response.status === 204) |  | ||||||
|             return resolve( |  | ||||||
|               rawResponse === true ? { response, body: null } : undefined |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|           const json: any = await response.json() |  | ||||||
|           await this.handleStatusCode(response, json, requestData, reject) |  | ||||||
| 
 |  | ||||||
|           if ( |  | ||||||
|             json.retry_after !== undefined || |  | ||||||
|             json.message === 'You are being rate limited.' |  | ||||||
|           ) { |  | ||||||
|             if (maxRetries > 10) { |  | ||||||
|               throw new Error('Max RateLimit Retries hit') |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             return { |  | ||||||
|               rateLimited: json.retry_after, |  | ||||||
|               before: false, |  | ||||||
|               bucket: bucketFromHeaders |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           return resolve(rawResponse === true ? { response, body: json } : json) |  | ||||||
|         } catch (error) { |  | ||||||
|           return reject(error) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       this.queue({ |  | ||||||
|         onComplete, |  | ||||||
|         bucket, |  | ||||||
|         url |  | ||||||
|       }) |  | ||||||
|       if (!this.processing) { |  | ||||||
|         this.processing = true |  | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |  | ||||||
|         this.processQueue() |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Checks for RateLimits times and deletes if already over */ |  | ||||||
|   private handleRateLimits(): void { |  | ||||||
|     const now = Date.now() |  | ||||||
|     this.rateLimits.forEach((value, key) => { |  | ||||||
|       // Ratelimit has not ended
 |  | ||||||
|       if (value.resetAt > now) return |  | ||||||
|       // It ended, so delete
 |  | ||||||
|       this.rateLimits.delete(key) |  | ||||||
|       if (key === 'global') this.globalRateLimit = false |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Makes a GET Request to API */ |  | ||||||
|   async get( |  | ||||||
|     url: string, |  | ||||||
|     body?: unknown, |  | ||||||
|     maxRetries = 0, |  | ||||||
|     bucket?: string | null, |  | ||||||
|     rawResponse?: boolean |  | ||||||
|   ): Promise<any> { |  | ||||||
|     return await this.make('get', url, body, maxRetries, bucket, rawResponse) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Makes a POST Request to API */ |  | ||||||
|   async post( |  | ||||||
|     url: string, |  | ||||||
|     body?: unknown, |  | ||||||
|     maxRetries = 0, |  | ||||||
|     bucket?: string | null, |  | ||||||
|     rawResponse?: boolean |  | ||||||
|   ): Promise<any> { |  | ||||||
|     return await this.make('post', url, body, maxRetries, bucket, rawResponse) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Makes a DELETE Request to API */ |  | ||||||
|   async delete( |  | ||||||
|     url: string, |  | ||||||
|     body?: unknown, |  | ||||||
|     maxRetries = 0, |  | ||||||
|     bucket?: string | null, |  | ||||||
|     rawResponse?: boolean |  | ||||||
|   ): Promise<any> { |  | ||||||
|     return await this.make('delete', url, body, maxRetries, bucket, rawResponse) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Makes a PATCH Request to API */ |  | ||||||
|   async patch( |  | ||||||
|     url: string, |  | ||||||
|     body?: unknown, |  | ||||||
|     maxRetries = 0, |  | ||||||
|     bucket?: string | null, |  | ||||||
|     rawResponse?: boolean |  | ||||||
|   ): Promise<any> { |  | ||||||
|     return await this.make('patch', url, body, maxRetries, bucket, rawResponse) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** Makes a PUT Request to API */ |  | ||||||
|   async put( |  | ||||||
|     url: string, |  | ||||||
|     body?: unknown, |  | ||||||
|     maxRetries = 0, |  | ||||||
|     bucket?: string | null, |  | ||||||
|     rawResponse?: boolean |  | ||||||
|   ): Promise<any> { |  | ||||||
|     return await this.make('put', url, body, maxRetries, bucket, rawResponse) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										239
									
								
								src/rest/bucket.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/rest/bucket.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,239 @@ | ||||||
|  | // based on https://github.com/discordjs/discord.js/blob/master/src/rest/RequestHandler.js
 | ||||||
|  | // adapted to work with harmony rest manager
 | ||||||
|  | 
 | ||||||
|  | /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ | ||||||
|  | 
 | ||||||
|  | import { delay } from '../utils/delay.ts' | ||||||
|  | import { DiscordAPIError, HTTPError } from './error.ts' | ||||||
|  | import type { RESTManager } from './manager.ts' | ||||||
|  | import { RequestQueue } from './queue.ts' | ||||||
|  | import { APIRequest } from './request.ts' | ||||||
|  | 
 | ||||||
|  | function parseResponse(res: Response, raw: boolean): any { | ||||||
|  |   if (raw) return res | ||||||
|  |   if (res.status === 204) return undefined | ||||||
|  |   if (res.headers.get('content-type')?.startsWith('application/json') === true) | ||||||
|  |     return res.json() | ||||||
|  |   return res.arrayBuffer().then((e) => new Uint8Array(e)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getAPIOffset(serverDate: number | string): number { | ||||||
|  |   return new Date(serverDate).getTime() - Date.now() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function calculateReset( | ||||||
|  |   reset: number | string, | ||||||
|  |   serverDate: number | string | ||||||
|  | ): number { | ||||||
|  |   return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let invalidCount = 0 | ||||||
|  | let invalidCountResetTime: number | null = null | ||||||
|  | 
 | ||||||
|  | export class BucketHandler { | ||||||
|  |   queue = new RequestQueue() | ||||||
|  |   reset = -1 | ||||||
|  |   remaining = -1 | ||||||
|  |   limit = -1 | ||||||
|  | 
 | ||||||
|  |   constructor(public manager: RESTManager) {} | ||||||
|  | 
 | ||||||
|  |   async push(request: APIRequest): Promise<any> { | ||||||
|  |     await this.queue.wait() | ||||||
|  |     try { | ||||||
|  |       return await this.execute(request) | ||||||
|  |     } finally { | ||||||
|  |       this.queue.shift() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get globalLimited(): boolean { | ||||||
|  |     return ( | ||||||
|  |       this.manager.globalRemaining <= 0 && | ||||||
|  |       Date.now() < Number(this.manager.globalReset) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get localLimited(): boolean { | ||||||
|  |     return this.remaining <= 0 && Date.now() < this.reset | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get limited(): boolean { | ||||||
|  |     return this.globalLimited || this.localLimited | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get inactive(): boolean { | ||||||
|  |     return this.queue.remaining === 0 && !this.limited | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async globalDelayFor(ms: number): Promise<void> { | ||||||
|  |     return await new Promise((resolve) => { | ||||||
|  |       this.manager.setTimeout(() => { | ||||||
|  |         this.manager.globalDelay = null | ||||||
|  |         resolve() | ||||||
|  |       }, ms) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async execute(request: APIRequest): Promise<any> { | ||||||
|  |     while (this.limited) { | ||||||
|  |       const isGlobal = this.globalLimited | ||||||
|  |       let limit, timeout, delayPromise | ||||||
|  | 
 | ||||||
|  |       if (isGlobal) { | ||||||
|  |         limit = this.manager.globalLimit | ||||||
|  |         timeout = | ||||||
|  |           // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
 | ||||||
|  |           Number(this.manager.globalReset) + | ||||||
|  |           this.manager.restTimeOffset - | ||||||
|  |           Date.now() | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
 | ||||||
|  |         if (!this.manager.globalDelay) { | ||||||
|  |           this.manager.globalDelay = this.globalDelayFor(timeout) as any | ||||||
|  |         } | ||||||
|  |         delayPromise = this.manager.globalDelay | ||||||
|  |       } else { | ||||||
|  |         limit = this.limit | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
 | ||||||
|  |         timeout = this.reset + this.manager.restTimeOffset - Date.now() | ||||||
|  |         delayPromise = delay(timeout) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.manager.client?.emit('rateLimit', { | ||||||
|  |         timeout, | ||||||
|  |         limit, | ||||||
|  |         method: request.method, | ||||||
|  |         path: request.path, | ||||||
|  |         global: isGlobal | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       await delayPromise | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
 | ||||||
|  |     if (!this.manager.globalReset || this.manager.globalReset < Date.now()) { | ||||||
|  |       this.manager.globalReset = Date.now() + 1000 | ||||||
|  |       this.manager.globalRemaining = this.manager.globalLimit | ||||||
|  |     } | ||||||
|  |     this.manager.globalRemaining-- | ||||||
|  | 
 | ||||||
|  |     // Perform the request
 | ||||||
|  |     let res | ||||||
|  |     try { | ||||||
|  |       res = await request.execute() | ||||||
|  |     } catch (error) { | ||||||
|  |       if (request.retries === this.manager.retryLimit) { | ||||||
|  |         throw new HTTPError( | ||||||
|  |           error.message, | ||||||
|  |           error.constructor.name, | ||||||
|  |           error.status, | ||||||
|  |           request.method, | ||||||
|  |           request.path | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       request.retries++ | ||||||
|  |       return await this.execute(request) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let sublimitTimeout | ||||||
|  |     if (res?.headers !== undefined) { | ||||||
|  |       const serverDate = res.headers.get('date') | ||||||
|  |       const limit = res.headers.get('x-ratelimit-limit') | ||||||
|  |       const remaining = res.headers.get('x-ratelimit-remaining') | ||||||
|  |       const reset = res.headers.get('x-ratelimit-reset') | ||||||
|  |       this.limit = limit !== null ? Number(limit) : Infinity | ||||||
|  |       this.remaining = remaining !== null ? Number(remaining) : 1 | ||||||
|  |       this.reset = | ||||||
|  |         reset !== null ? calculateReset(reset, serverDate!) : Date.now() | ||||||
|  | 
 | ||||||
|  |       if (request.path.includes('reactions') === true) { | ||||||
|  |         this.reset = | ||||||
|  |           new Date(serverDate!).getTime() - getAPIOffset(serverDate!) + 250 | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       let retryAfter: number | null | string = res.headers.get('retry-after') | ||||||
|  |       retryAfter = retryAfter !== null ? Number(retryAfter) * 1000 : -1 | ||||||
|  |       if (retryAfter > 0) { | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
 | ||||||
|  |         if (res.headers.get('x-ratelimit-global')) { | ||||||
|  |           this.manager.globalRemaining = 0 | ||||||
|  |           this.manager.globalReset = Date.now() + retryAfter | ||||||
|  |         } else if (!this.localLimited) { | ||||||
|  |           sublimitTimeout = retryAfter | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (res.status === 401 || res.status === 403 || res.status === 429) { | ||||||
|  |       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
 | ||||||
|  |       if (!invalidCountResetTime || invalidCountResetTime < Date.now()) { | ||||||
|  |         invalidCountResetTime = Date.now() + 1000 * 60 * 10 | ||||||
|  |         invalidCount = 0 | ||||||
|  |       } | ||||||
|  |       // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  |       invalidCount++ | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (res.ok === true) { | ||||||
|  |       return parseResponse(res, request.options.rawResponse ?? false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (res.status >= 400 && res.status < 500) { | ||||||
|  |       if (res.status === 429) { | ||||||
|  |         this.manager.client?.emit( | ||||||
|  |           'debug', | ||||||
|  |           `Rate-Limited on route ${request.path}${ | ||||||
|  |             sublimitTimeout !== undefined ? ' for sublimit' : '' | ||||||
|  |           }` | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         if (sublimitTimeout !== undefined) { | ||||||
|  |           await delay(sublimitTimeout) | ||||||
|  |         } | ||||||
|  |         return await this.execute(request) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       let data | ||||||
|  |       try { | ||||||
|  |         data = await parseResponse(res, request.options.rawResponse ?? false) | ||||||
|  |       } catch (err) { | ||||||
|  |         throw new HTTPError( | ||||||
|  |           err.message, | ||||||
|  |           err.constructor.name, | ||||||
|  |           err.status, | ||||||
|  |           request.method, | ||||||
|  |           request.path | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       throw new DiscordAPIError({ | ||||||
|  |         url: request.path, | ||||||
|  |         errors: data?.errors, | ||||||
|  |         status: res.status, | ||||||
|  |         method: request.method, | ||||||
|  |         message: data?.message, | ||||||
|  |         code: data?.code, | ||||||
|  |         requestData: request.options.data | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (res.status >= 500 && res.status < 600) { | ||||||
|  |       if (request.retries === this.manager.retryLimit) { | ||||||
|  |         throw new HTTPError( | ||||||
|  |           res.statusText, | ||||||
|  |           res.constructor.name, | ||||||
|  |           res.status, | ||||||
|  |           request.method, | ||||||
|  |           request.path | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       request.retries++ | ||||||
|  |       return await this.execute(request) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1294
									
								
								src/rest/endpoints.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1294
									
								
								src/rest/endpoints.ts
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										44
									
								
								src/rest/error.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/rest/error.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | import { simplifyAPIError } from '../utils/err_fmt.ts' | ||||||
|  | import { DiscordAPIErrorPayload } from './types.ts' | ||||||
|  | 
 | ||||||
|  | export class DiscordAPIError extends Error { | ||||||
|  |   name = 'DiscordAPIError' | ||||||
|  |   error?: DiscordAPIErrorPayload | ||||||
|  | 
 | ||||||
|  |   constructor(error: string | DiscordAPIErrorPayload) { | ||||||
|  |     super() | ||||||
|  |     const fmt = Object.entries( | ||||||
|  |       typeof error === 'object' ? simplifyAPIError(error.errors ?? {}) : {} | ||||||
|  |     ) | ||||||
|  |     this.message = | ||||||
|  |       typeof error === 'string' | ||||||
|  |         ? `${error} ` | ||||||
|  |         : `\n${error.method.toUpperCase()} ${error.url.slice(7)} returned ${ | ||||||
|  |             error.status | ||||||
|  |           }\n(${error.code ?? 'unknown'}) ${error.message}${ | ||||||
|  |             fmt.length === 0 | ||||||
|  |               ? '' | ||||||
|  |               : `\n${fmt | ||||||
|  |                   .map( | ||||||
|  |                     (e) => | ||||||
|  |                       `  at ${e[0]}:\n${e[1] | ||||||
|  |                         .map((e) => `   - ${e}`) | ||||||
|  |                         .join('\n')}` | ||||||
|  |                   ) | ||||||
|  |                   .join('\n')}\n` | ||||||
|  |           }` | ||||||
|  |     if (typeof error === 'object') this.error = error | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class HTTPError extends Error { | ||||||
|  |   constructor( | ||||||
|  |     public message: string, | ||||||
|  |     public name: string, | ||||||
|  |     public code: number, | ||||||
|  |     public method: string, | ||||||
|  |     public path: string | ||||||
|  |   ) { | ||||||
|  |     super(message) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										302
									
								
								src/rest/manager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								src/rest/manager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,302 @@ | ||||||
|  | import { Collection } from '../utils/collection.ts' | ||||||
|  | import type { Client } from '../client/mod.ts' | ||||||
|  | import { RequestMethods, METHODS } from './types.ts' | ||||||
|  | import { Constants } from '../types/constants.ts' | ||||||
|  | import { RESTEndpoints } from './endpoints.ts' | ||||||
|  | import { BucketHandler } from './bucket.ts' | ||||||
|  | import { APIRequest, RequestOptions } from './request.ts' | ||||||
|  | 
 | ||||||
|  | export type MethodFunction = ( | ||||||
|  |   body?: unknown, | ||||||
|  |   maxRetries?: number, | ||||||
|  |   bucket?: string | null, | ||||||
|  |   rawResponse?: boolean, | ||||||
|  |   options?: RequestOptions | ||||||
|  | ) => Promise<any> | ||||||
|  | 
 | ||||||
|  | export interface APIMap extends MethodFunction { | ||||||
|  |   /** Make a GET request to current route */ | ||||||
|  |   get: APIMap | ||||||
|  |   /** Make a POST request to current route */ | ||||||
|  |   post: APIMap | ||||||
|  |   /** Make a PATCH request to current route */ | ||||||
|  |   patch: APIMap | ||||||
|  |   /** Make a PUT request to current route */ | ||||||
|  |   put: APIMap | ||||||
|  |   /** Make a DELETE request to current route */ | ||||||
|  |   delete: APIMap | ||||||
|  |   /** Make a HEAD request to current route */ | ||||||
|  |   head: APIMap | ||||||
|  |   /** Continue building API Route */ | ||||||
|  |   [name: string]: APIMap | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** API Route builder function */ | ||||||
|  | export const builder = (rest: RESTManager, acum = '/'): APIMap => { | ||||||
|  |   const routes = {} | ||||||
|  |   const proxy = new Proxy(routes, { | ||||||
|  |     get: (_, p, __) => { | ||||||
|  |       if (p === 'toString') return () => acum | ||||||
|  |       if (METHODS.includes(String(p)) === true) { | ||||||
|  |         const method = ((rest as unknown) as { | ||||||
|  |           [name: string]: MethodFunction | ||||||
|  |         })[String(p)] | ||||||
|  |         return async (...args: any[]) => | ||||||
|  |           await method.bind(rest)( | ||||||
|  |             `${Constants.DISCORD_API_URL}/v${rest.version}${acum.substring( | ||||||
|  |               0, | ||||||
|  |               acum.length - 1 | ||||||
|  |             )}`,
 | ||||||
|  |             ...args | ||||||
|  |           ) | ||||||
|  |       } | ||||||
|  |       return builder(rest, acum + String(p) + '/') | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   return (proxy as unknown) as APIMap | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface RESTOptions { | ||||||
|  |   /** Token to use for authorization */ | ||||||
|  |   token?: string | (() => string | undefined) | ||||||
|  |   /** Headers to patch with if any */ | ||||||
|  |   headers?: { [name: string]: string | undefined } | ||||||
|  |   /** Whether to use Canary instance of Discord API or not */ | ||||||
|  |   canary?: boolean | ||||||
|  |   /** Discord REST API version to use */ | ||||||
|  |   version?: 6 | 7 | 8 | ||||||
|  |   /** Token Type to use for Authorization */ | ||||||
|  |   tokenType?: TokenType | ||||||
|  |   /** User Agent to use (Header) */ | ||||||
|  |   userAgent?: string | ||||||
|  |   /** Optional Harmony client */ | ||||||
|  |   client?: Client | ||||||
|  |   /** Requests Timeout (in MS, default 30s) */ | ||||||
|  |   requestTimeout?: number | ||||||
|  |   /** Retry Limit (default 1) */ | ||||||
|  |   retryLimit?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Token Type for REST API. */ | ||||||
|  | export enum TokenType { | ||||||
|  |   /** Token type for Bot User */ | ||||||
|  |   Bot = 'Bot', | ||||||
|  |   /** Token Type for OAuth2 */ | ||||||
|  |   Bearer = 'Bearer', | ||||||
|  |   /** No Token Type. Can be used for User accounts. */ | ||||||
|  |   None = '' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** An easier to use interface for interacting with Discord REST API. */ | ||||||
|  | export class RESTManager { | ||||||
|  |   /** API Version being used by REST Manager */ | ||||||
|  |   version: number = 8 | ||||||
|  |   /** | ||||||
|  |    * API Map - easy to use way for interacting with Discord API. | ||||||
|  |    * | ||||||
|  |    * Examples: | ||||||
|  |    * * ```ts
 | ||||||
|  |    *   rest.api.users['123'].get().then(userPayload => doSomething) | ||||||
|  |    *   ``` | ||||||
|  |    * * ```ts
 | ||||||
|  |    *   rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {}) | ||||||
|  |    *   ``` | ||||||
|  |    */ | ||||||
|  |   api: APIMap | ||||||
|  |   /** Token being used for Authorization */ | ||||||
|  |   token?: string | (() => string | undefined) | ||||||
|  |   /** Token Type of the Token if any */ | ||||||
|  |   tokenType: TokenType = TokenType.Bot | ||||||
|  |   /** Headers object which patch the current ones */ | ||||||
|  |   headers: any = {} | ||||||
|  |   /** Optional custom User Agent (header) */ | ||||||
|  |   userAgent?: string | ||||||
|  |   /** Whether REST Manager is using Canary API */ | ||||||
|  |   canary?: boolean | ||||||
|  |   /** Optional Harmony Client object */ | ||||||
|  |   client?: Client | ||||||
|  |   endpoints: RESTEndpoints | ||||||
|  |   requestTimeout = 30000 | ||||||
|  |   timers: Set<number> = new Set() | ||||||
|  |   apiURL = Constants.DISCORD_API_URL | ||||||
|  | 
 | ||||||
|  |   handlers = new Collection<string, BucketHandler>() | ||||||
|  |   globalLimit = Infinity | ||||||
|  |   globalRemaining = this.globalLimit | ||||||
|  |   globalReset: number | null = null | ||||||
|  |   globalDelay: number | null = null | ||||||
|  |   retryLimit = 1 | ||||||
|  |   restTimeOffset = 0 | ||||||
|  | 
 | ||||||
|  |   constructor(options?: RESTOptions) { | ||||||
|  |     this.api = builder(this) | ||||||
|  |     if (options?.token !== undefined) this.token = options.token | ||||||
|  |     if (options?.version !== undefined) this.version = options.version | ||||||
|  |     if (options?.headers !== undefined) this.headers = options.headers | ||||||
|  |     if (options?.tokenType !== undefined) this.tokenType = options.tokenType | ||||||
|  |     if (options?.userAgent !== undefined) this.userAgent = options.userAgent | ||||||
|  |     if (options?.canary !== undefined) this.canary = options.canary | ||||||
|  |     if (options?.client !== undefined) this.client = options.client | ||||||
|  |     if (options?.retryLimit !== undefined) this.retryLimit = options.retryLimit | ||||||
|  |     if (options?.requestTimeout !== undefined) | ||||||
|  |       this.requestTimeout = options.requestTimeout | ||||||
|  |     this.endpoints = new RESTEndpoints(this) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setTimeout(fn: (...args: any[]) => any, ms: number): number { | ||||||
|  |     const timer = setTimeout(async () => { | ||||||
|  |       this.timers.delete(timer) | ||||||
|  |       await fn() | ||||||
|  |     }, ms) | ||||||
|  |     this.timers.add(timer) | ||||||
|  |     return timer | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async request<T = any>( | ||||||
|  |     method: RequestMethods, | ||||||
|  |     path: string, | ||||||
|  |     options: RequestOptions = {} | ||||||
|  |   ): Promise<T> { | ||||||
|  |     const req = new APIRequest(this, method, path, options) | ||||||
|  |     let handler = this.handlers.get(req.path) | ||||||
|  | 
 | ||||||
|  |     if (handler === undefined) { | ||||||
|  |       handler = new BucketHandler(this) | ||||||
|  |       this.handlers.set(req.route, handler) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return handler.push(req) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Makes a Request to Discord API. | ||||||
|  |    * @param method HTTP Method to use | ||||||
|  |    * @param url URL of the Request | ||||||
|  |    * @param body Body to send with Request | ||||||
|  |    * @param maxRetries Number of Max Retries to perform | ||||||
|  |    * @param bucket BucketID of the Request | ||||||
|  |    * @param rawResponse Whether to get Raw Response or body itself | ||||||
|  |    */ | ||||||
|  |   async make( | ||||||
|  |     method: RequestMethods, | ||||||
|  |     url: string, | ||||||
|  |     body?: unknown, | ||||||
|  |     _maxRetries = 0, | ||||||
|  |     bucket?: string | null, | ||||||
|  |     rawResponse?: boolean, | ||||||
|  |     options: RequestOptions = {} | ||||||
|  |   ): Promise<any> { | ||||||
|  |     return await this.request( | ||||||
|  |       method, | ||||||
|  |       url, | ||||||
|  |       Object.assign( | ||||||
|  |         { | ||||||
|  |           data: body, | ||||||
|  |           rawResponse, | ||||||
|  |           route: bucket ?? undefined | ||||||
|  |         }, | ||||||
|  |         options | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Makes a GET Request to API */ | ||||||
|  |   async get( | ||||||
|  |     url: string, | ||||||
|  |     body?: unknown, | ||||||
|  |     maxRetries = 0, | ||||||
|  |     bucket?: string | null, | ||||||
|  |     rawResponse?: boolean, | ||||||
|  |     options?: RequestOptions | ||||||
|  |   ): Promise<any> { | ||||||
|  |     return await this.make( | ||||||
|  |       'get', | ||||||
|  |       url, | ||||||
|  |       body, | ||||||
|  |       maxRetries, | ||||||
|  |       bucket, | ||||||
|  |       rawResponse, | ||||||
|  |       options | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Makes a POST Request to API */ | ||||||
|  |   async post( | ||||||
|  |     url: string, | ||||||
|  |     body?: unknown, | ||||||
|  |     maxRetries = 0, | ||||||
|  |     bucket?: string | null, | ||||||
|  |     rawResponse?: boolean, | ||||||
|  |     options?: RequestOptions | ||||||
|  |   ): Promise<any> { | ||||||
|  |     return await this.make( | ||||||
|  |       'post', | ||||||
|  |       url, | ||||||
|  |       body, | ||||||
|  |       maxRetries, | ||||||
|  |       bucket, | ||||||
|  |       rawResponse, | ||||||
|  |       options | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Makes a DELETE Request to API */ | ||||||
|  |   async delete( | ||||||
|  |     url: string, | ||||||
|  |     body?: unknown, | ||||||
|  |     maxRetries = 0, | ||||||
|  |     bucket?: string | null, | ||||||
|  |     rawResponse?: boolean, | ||||||
|  |     options?: RequestOptions | ||||||
|  |   ): Promise<any> { | ||||||
|  |     return await this.make( | ||||||
|  |       'delete', | ||||||
|  |       url, | ||||||
|  |       body, | ||||||
|  |       maxRetries, | ||||||
|  |       bucket, | ||||||
|  |       rawResponse, | ||||||
|  |       options | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Makes a PATCH Request to API */ | ||||||
|  |   async patch( | ||||||
|  |     url: string, | ||||||
|  |     body?: unknown, | ||||||
|  |     maxRetries = 0, | ||||||
|  |     bucket?: string | null, | ||||||
|  |     rawResponse?: boolean, | ||||||
|  |     options?: RequestOptions | ||||||
|  |   ): Promise<any> { | ||||||
|  |     return await this.make( | ||||||
|  |       'patch', | ||||||
|  |       url, | ||||||
|  |       body, | ||||||
|  |       maxRetries, | ||||||
|  |       bucket, | ||||||
|  |       rawResponse, | ||||||
|  |       options | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Makes a PUT Request to API */ | ||||||
|  |   async put( | ||||||
|  |     url: string, | ||||||
|  |     body?: unknown, | ||||||
|  |     maxRetries = 0, | ||||||
|  |     bucket?: string | null, | ||||||
|  |     rawResponse?: boolean, | ||||||
|  |     options?: RequestOptions | ||||||
|  |   ): Promise<any> { | ||||||
|  |     return await this.make( | ||||||
|  |       'put', | ||||||
|  |       url, | ||||||
|  |       body, | ||||||
|  |       maxRetries, | ||||||
|  |       bucket, | ||||||
|  |       rawResponse, | ||||||
|  |       options | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								src/rest/mod.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/rest/mod.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | export * from './manager.ts' | ||||||
|  | export * from './types.ts' | ||||||
|  | export * from './endpoints.ts' | ||||||
|  | export * from './error.ts' | ||||||
|  | export * from './bucket.ts' | ||||||
|  | export * from './queue.ts' | ||||||
|  | export * from './request.ts' | ||||||
							
								
								
									
										37
									
								
								src/rest/queue.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/rest/queue.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | // based on https://github.com/discordjs/discord.js/blob/master/src/rest/AsyncQueue.js
 | ||||||
|  | 
 | ||||||
|  | export interface RequestPromise { | ||||||
|  |   resolve: CallableFunction | ||||||
|  |   promise: Promise<any> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class RequestQueue { | ||||||
|  |   promises: RequestPromise[] = [] | ||||||
|  | 
 | ||||||
|  |   get remaining(): number { | ||||||
|  |     return this.promises.length | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async wait(): Promise<any> { | ||||||
|  |     const next = | ||||||
|  |       this.promises.length !== 0 | ||||||
|  |         ? this.promises[this.promises.length - 1].promise | ||||||
|  |         : Promise.resolve() | ||||||
|  |     let resolveFn: CallableFunction | undefined | ||||||
|  |     const promise = new Promise((resolve) => { | ||||||
|  |       resolveFn = resolve | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     this.promises.push({ | ||||||
|  |       resolve: resolveFn!, | ||||||
|  |       promise | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     return next | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   shift(): void { | ||||||
|  |     const deferred = this.promises.shift() | ||||||
|  |     if (typeof deferred !== 'undefined') deferred.resolve() | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								src/rest/request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/rest/request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | ||||||
|  | import type { Embed } from '../structures/embed.ts' | ||||||
|  | import type { MessageAttachment } from '../structures/message.ts' | ||||||
|  | import type { RESTManager } from './manager.ts' | ||||||
|  | import type { RequestMethods } from './types.ts' | ||||||
|  | 
 | ||||||
|  | export interface RequestOptions { | ||||||
|  |   headers?: { [name: string]: string } | ||||||
|  |   query?: { [name: string]: string } | ||||||
|  |   files?: MessageAttachment[] | ||||||
|  |   data?: any | ||||||
|  |   reason?: string | ||||||
|  |   rawResponse?: boolean | ||||||
|  |   route?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class APIRequest { | ||||||
|  |   retries = 0 | ||||||
|  |   route: string | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     public rest: RESTManager, | ||||||
|  |     public method: RequestMethods, | ||||||
|  |     public path: string, | ||||||
|  |     public options: RequestOptions | ||||||
|  |   ) { | ||||||
|  |     this.route = options.route ?? path | ||||||
|  |     if (typeof options.query === 'object') { | ||||||
|  |       const entries = Object.entries(options.query) | ||||||
|  |       if (entries.length > 0) { | ||||||
|  |         this.path += '?' | ||||||
|  |         entries.forEach((entry, i) => { | ||||||
|  |           this.path += `${i === 0 ? '' : '&'}${encodeURIComponent( | ||||||
|  |             entry[0] | ||||||
|  |           )}=${encodeURIComponent(entry[1])}` | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let _files: undefined | MessageAttachment[] | ||||||
|  |     if ( | ||||||
|  |       options.data?.embed?.files !== undefined && | ||||||
|  |       Array.isArray(options.data?.embed?.files) | ||||||
|  |     ) { | ||||||
|  |       _files = [...options.data?.embed?.files] | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       options.data?.embeds !== undefined && | ||||||
|  |       Array.isArray(options.data?.embeds) | ||||||
|  |     ) { | ||||||
|  |       const files1 = options.data?.embeds | ||||||
|  |         .map((e: Embed) => e.files) | ||||||
|  |         .filter((e: MessageAttachment[]) => e !== undefined) | ||||||
|  |       for (const files of files1) { | ||||||
|  |         for (const file of files) { | ||||||
|  |           if (_files === undefined) _files = [] | ||||||
|  |           _files?.push(file) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (options.data?.file !== undefined) { | ||||||
|  |       if (_files === undefined) _files = [] | ||||||
|  |       _files.push(options.data?.file) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       options.data?.files !== undefined && | ||||||
|  |       Array.isArray(options.data?.files) | ||||||
|  |     ) { | ||||||
|  |       if (_files === undefined) _files = [] | ||||||
|  |       options.data?.files.forEach((file: any) => { | ||||||
|  |         _files!.push(file) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (_files !== undefined && _files.length > 0) { | ||||||
|  |       if (options.files === undefined) options.files = _files | ||||||
|  |       else options.files = [...options.files, ..._files] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async execute(): Promise<Response> { | ||||||
|  |     let contentType: string | undefined | ||||||
|  |     let body: any = this.options.data | ||||||
|  |     if (this.options.files !== undefined && this.options.files.length > 0) { | ||||||
|  |       contentType = undefined | ||||||
|  |       const form = new FormData() | ||||||
|  |       this.options.files.forEach((file, i) => | ||||||
|  |         form.append(`file${i === 0 ? '' : i}`, file.blob, file.name) | ||||||
|  |       ) | ||||||
|  |       form.append('payload_json', JSON.stringify(body)) | ||||||
|  |       body = form | ||||||
|  |     } else { | ||||||
|  |       contentType = 'application/json' | ||||||
|  |       body = JSON.stringify(body) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const controller = new AbortController() | ||||||
|  |     const timer = setTimeout(() => { | ||||||
|  |       controller.abort() | ||||||
|  |     }, this.rest.requestTimeout) | ||||||
|  |     this.rest.timers.add(timer) | ||||||
|  | 
 | ||||||
|  |     const url = this.path.startsWith('http') | ||||||
|  |       ? this.path | ||||||
|  |       : `${this.rest.apiURL}/v${this.rest.version}${this.path}` | ||||||
|  | 
 | ||||||
|  |     const headers: any = { | ||||||
|  |       'User-Agent': | ||||||
|  |         this.rest.userAgent ?? | ||||||
|  |         `DiscordBot (harmony, https://github.com/harmonyland/harmony)`, | ||||||
|  |       Authorization: | ||||||
|  |         this.rest.token === undefined | ||||||
|  |           ? undefined | ||||||
|  |           : `${this.rest.tokenType} ${this.rest.token}`.trim() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (contentType !== undefined) headers['Content-Type'] = contentType | ||||||
|  | 
 | ||||||
|  |     const init: RequestInit = { | ||||||
|  |       method: this.method.toUpperCase(), | ||||||
|  |       signal: controller.signal, | ||||||
|  |       headers: Object.assign(headers, this.rest.headers, this.options.headers), | ||||||
|  |       body | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return fetch(url, init).finally(() => { | ||||||
|  |       clearTimeout(timer) | ||||||
|  |       this.rest.timers.delete(timer) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								src/rest/types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/rest/types.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | export type RequestMethods = | ||||||
|  |   | 'get' | ||||||
|  |   | 'post' | ||||||
|  |   | 'put' | ||||||
|  |   | 'patch' | ||||||
|  |   | 'head' | ||||||
|  |   | 'delete' | ||||||
|  | 
 | ||||||
|  | export enum HttpResponseCode { | ||||||
|  |   Ok = 200, | ||||||
|  |   Created = 201, | ||||||
|  |   NoContent = 204, | ||||||
|  |   NotModified = 304, | ||||||
|  |   BadRequest = 400, | ||||||
|  |   Unauthorized = 401, | ||||||
|  |   Forbidden = 403, | ||||||
|  |   NotFound = 404, | ||||||
|  |   MethodNotAllowed = 405, | ||||||
|  |   TooManyRequests = 429, | ||||||
|  |   GatewayUnavailable = 502 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface RequestHeaders { | ||||||
|  |   [name: string]: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface DiscordAPIErrorPayload { | ||||||
|  |   url: string | ||||||
|  |   status: number | ||||||
|  |   method: string | ||||||
|  |   code?: number | ||||||
|  |   message?: string | ||||||
|  |   errors: object | ||||||
|  |   requestData: { [key: string]: any } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head'] | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { ApplicationPayload } from '../types/application.ts' | import type { ApplicationPayload } from '../types/application.ts' | ||||||
| import { SnowflakeBase } from './base.ts' | import { SnowflakeBase } from './base.ts' | ||||||
| import { User } from './user.ts' | import { User } from './user.ts' | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { Snowflake } from '../utils/snowflake.ts' | import { Snowflake } from '../utils/snowflake.ts' | ||||||
| 
 | 
 | ||||||
| export class Base { | export class Base { | ||||||
|   client: Client |   client!: Client | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, _data?: any) { |   constructor(client: Client, _data?: any) { | ||||||
|     this.client = client |     Object.defineProperty(this, 'client', { value: client, enumerable: false }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { ImageFormats, ImageSize } from '../types/cdn.ts' | import type { ImageFormats, ImageSize } from '../types/cdn.ts' | ||||||
| 
 | 
 | ||||||
| /** Function to get Image URL from a resource on Discord CDN */ | /** Function to get Image URL from a resource on Discord CDN */ | ||||||
| export const ImageURL = ( | export const ImageURL = ( | ||||||
|  |  | ||||||
|  | @ -1,20 +1,20 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { | import type { | ||||||
|   ChannelPayload, |   ChannelPayload, | ||||||
|   ChannelTypes, |   ChannelTypes, | ||||||
|   ModifyChannelOption, |   ModifyChannelOption, | ||||||
|   ModifyChannelPayload, |   ModifyChannelPayload, | ||||||
|   Overwrite, |   Overwrite, | ||||||
|   OverwritePayload, |   OverwritePayload, | ||||||
|   OverwriteAsArg, |   OverwriteAsArg | ||||||
|   OverrideType |  | ||||||
| } from '../types/channel.ts' | } from '../types/channel.ts' | ||||||
|  | import { OverrideType } from '../types/channel.ts' | ||||||
| import { CHANNEL } from '../types/endpoint.ts' | import { CHANNEL } from '../types/endpoint.ts' | ||||||
| import { GuildChannelPayloads, GuildChannels } from '../types/guild.ts' | import type { GuildChannelPayloads, GuildChannels } from '../types/guild.ts' | ||||||
| import getChannelByType from '../utils/getChannelByType.ts' | import getChannelByType from '../utils/channel.ts' | ||||||
| import { Permissions } from '../utils/permissions.ts' | import { Permissions } from '../utils/permissions.ts' | ||||||
| import { SnowflakeBase } from './base.ts' | import { SnowflakeBase } from './base.ts' | ||||||
| import { Guild } from './guild.ts' | import type { Guild } from './guild.ts' | ||||||
| import { Member } from './member.ts' | import { Member } from './member.ts' | ||||||
| import { Role } from './role.ts' | import { Role } from './role.ts' | ||||||
| 
 | 
 | ||||||
|  | @ -81,7 +81,7 @@ export class GuildChannel extends Channel { | ||||||
|     const stringToObject = |     const stringToObject = | ||||||
|       typeof target === 'string' |       typeof target === 'string' | ||||||
|         ? (await this.guild.members.get(target)) ?? |         ? (await this.guild.members.get(target)) ?? | ||||||
|         (await this.guild.roles.get(target)) |           (await this.guild.roles.get(target)) | ||||||
|         : target |         : target | ||||||
| 
 | 
 | ||||||
|     if (stringToObject === undefined) { |     if (stringToObject === undefined) { | ||||||
|  | @ -128,7 +128,7 @@ export class GuildChannel extends Channel { | ||||||
|     const stringToObject = |     const stringToObject = | ||||||
|       typeof target === 'string' |       typeof target === 'string' | ||||||
|         ? (await this.guild.members.get(target)) ?? |         ? (await this.guild.members.get(target)) ?? | ||||||
|         (await this.guild.roles.get(target)) |           (await this.guild.roles.get(target)) | ||||||
|         : target |         : target | ||||||
| 
 | 
 | ||||||
|     if (stringToObject === undefined) { |     if (stringToObject === undefined) { | ||||||
|  | @ -200,8 +200,8 @@ export class GuildChannel extends Channel { | ||||||
|           overwrite.id instanceof Role |           overwrite.id instanceof Role | ||||||
|             ? 0 |             ? 0 | ||||||
|             : overwrite.id instanceof Member |             : overwrite.id instanceof Member | ||||||
|               ? 1 |             ? 1 | ||||||
|               : overwrite.type |             : overwrite.type | ||||||
|         if (type === undefined) { |         if (type === undefined) { | ||||||
|           throw new Error('Overwrite type is undefined.') |           throw new Error('Overwrite type is undefined.') | ||||||
|         } |         } | ||||||
|  | @ -233,8 +233,8 @@ export class GuildChannel extends Channel { | ||||||
|       overwrite.id instanceof Role |       overwrite.id instanceof Role | ||||||
|         ? 0 |         ? 0 | ||||||
|         : overwrite.id instanceof Member |         : overwrite.id instanceof Member | ||||||
|           ? 1 |         ? 1 | ||||||
|           : overwrite.type |         : overwrite.type | ||||||
|     if (type === undefined) { |     if (type === undefined) { | ||||||
|       throw new Error('Overwrite type is undefined.') |       throw new Error('Overwrite type is undefined.') | ||||||
|     } |     } | ||||||
|  | @ -303,7 +303,10 @@ export class GuildChannel extends Channel { | ||||||
|           : overwrite.allow?.toJSON() ?? overwrites[index].allow |           : overwrite.allow?.toJSON() ?? overwrites[index].allow | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (overwrite.deny !== undefined && overwriteDeny !== OverrideType.REPLACE) { |     if ( | ||||||
|  |       overwrite.deny !== undefined && | ||||||
|  |       overwriteDeny !== OverrideType.REPLACE | ||||||
|  |     ) { | ||||||
|       switch (overwriteDeny) { |       switch (overwriteDeny) { | ||||||
|         case OverrideType.ADD: { |         case OverrideType.ADD: { | ||||||
|           const originalDeny = new Permissions(overwrites[index].deny) |           const originalDeny = new Permissions(overwrites[index].deny) | ||||||
|  | @ -331,8 +334,8 @@ export class GuildChannel extends Channel { | ||||||
|       overwrite.id instanceof Role |       overwrite.id instanceof Role | ||||||
|         ? 0 |         ? 0 | ||||||
|         : overwrite.id instanceof Member |         : overwrite.id instanceof Member | ||||||
|           ? 1 |         ? 1 | ||||||
|           : overwrite.type |         : overwrite.type | ||||||
|     if (type === undefined) { |     if (type === undefined) { | ||||||
|       throw new Error('Overwrite type is undefined.') |       throw new Error('Overwrite type is undefined.') | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Client } from '../models/client.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import { DMChannelPayload } from '../types/channel.ts' | import type { DMChannelPayload } from '../types/channel.ts' | ||||||
| import { UserPayload } from '../types/user.ts' | import type { UserPayload } from '../types/user.ts' | ||||||
| import { TextChannel } from './textChannel.ts' | import { TextChannel } from './textChannel.ts' | ||||||
| 
 | 
 | ||||||
| export class DMChannel extends TextChannel { | export class DMChannel extends TextChannel { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { | import type { | ||||||
|   EmbedAuthor, |   EmbedAuthor, | ||||||
|   EmbedField, |   EmbedField, | ||||||
|   EmbedFooter, |   EmbedFooter, | ||||||
|  | @ -10,7 +10,7 @@ import { | ||||||
|   EmbedVideo |   EmbedVideo | ||||||
| } from '../types/channel.ts' | } from '../types/channel.ts' | ||||||
| import { Colors, ColorUtil } from '../utils/colorutil.ts' | import { Colors, ColorUtil } from '../utils/colorutil.ts' | ||||||
| import { MessageAttachment } from './message.ts' | import type { MessageAttachment } from './message.ts' | ||||||
| 
 | 
 | ||||||
| /** Message Embed Object */ | /** Message Embed Object */ | ||||||
| export class Embed { | export class Embed { | ||||||
|  | @ -56,44 +56,72 @@ export class Embed { | ||||||
| 
 | 
 | ||||||
|   /** Convert Embed Object to Embed Payload JSON */ |   /** Convert Embed Object to Embed Payload JSON */ | ||||||
|   toJSON(): EmbedPayload { |   toJSON(): EmbedPayload { | ||||||
|     let total = 0; |     let total = 0 | ||||||
|     if (this.title?.length !== undefined && this.title?.length > Embed.MAX_TITLE_LENGTH) { |     if ( | ||||||
|  |       this.title?.length !== undefined && | ||||||
|  |       this.title?.length > Embed.MAX_TITLE_LENGTH | ||||||
|  |     ) { | ||||||
|       total += Number(this.title.length) |       total += Number(this.title.length) | ||||||
|       throw new Error(`Embed title cannot exceed ${Embed.MAX_TITLE_LENGTH} characters.`) |       throw new Error( | ||||||
|  |         `Embed title cannot exceed ${Embed.MAX_TITLE_LENGTH} characters.` | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
|   | 
 | ||||||
|     if (this.description?.length !== undefined && this.description?.length > Embed.MAX_DESCRIPTION_LENGTH) { |     if ( | ||||||
|  |       this.description?.length !== undefined && | ||||||
|  |       this.description?.length > Embed.MAX_DESCRIPTION_LENGTH | ||||||
|  |     ) { | ||||||
|       total += Number(this.description.length) |       total += Number(this.description.length) | ||||||
|       throw new Error(`Embed description cannot exceed ${Embed.MAX_DESCRIPTION_LENGTH} characters.`) |       throw new Error( | ||||||
|  |         `Embed description cannot exceed ${Embed.MAX_DESCRIPTION_LENGTH} characters.` | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
|   | 
 | ||||||
|     if (this.fields?.length !== undefined) { |     if (this.fields?.length !== undefined) { | ||||||
|       this.fields.forEach((field) => { |       this.fields.forEach((field) => { | ||||||
|         if (field.name.length > Embed.MAX_FIELD_NAME_LENGTH) { |         if (field.name.length > Embed.MAX_FIELD_NAME_LENGTH) { | ||||||
|           total += Number(field.name.length) |           total += Number(field.name.length) | ||||||
|           throw new Error(`Embed field name cannot exceed ${Embed.MAX_FIELD_NAME_LENGTH} characters.`) |           throw new Error( | ||||||
|  |             `Embed field name cannot exceed ${Embed.MAX_FIELD_NAME_LENGTH} characters.` | ||||||
|  |           ) | ||||||
|         } |         } | ||||||
|   | 
 | ||||||
|         if (field.value.length > Embed.MAX_FIELD_VALUE_LENGTH) { |         if (field.value.length > Embed.MAX_FIELD_VALUE_LENGTH) { | ||||||
|           total += Number(field.value.length) |           total += Number(field.value.length) | ||||||
|           throw new Error(`Embed field value cannot exceed ${Embed.MAX_FIELD_VALUE_LENGTH} characters.`) |           throw new Error( | ||||||
|  |             `Embed field value cannot exceed ${Embed.MAX_FIELD_VALUE_LENGTH} characters.` | ||||||
|  |           ) | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       if (this.fields.length > Embed.MAX_FIELDS_LENGTH) throw new Error('Embed fields cannot exceed 25 field objects.') |       if (this.fields.length > Embed.MAX_FIELDS_LENGTH) | ||||||
|  |         throw new Error('Embed fields cannot exceed 25 field objects.') | ||||||
|     } |     } | ||||||
|   | 
 | ||||||
|     if (this.footer?.text?.length !== undefined && this.footer?.text?.length > Embed.MAX_FOOTER_TEXT_LENGTH) { |     if ( | ||||||
|  |       this.footer?.text?.length !== undefined && | ||||||
|  |       this.footer?.text?.length > Embed.MAX_FOOTER_TEXT_LENGTH | ||||||
|  |     ) { | ||||||
|       total += Number(this.footer?.text?.length) |       total += Number(this.footer?.text?.length) | ||||||
|       throw new Error(`Embed footer text cannot exceed ${Embed.MAX_FOOTER_TEXT_LENGTH}.`) |       throw new Error( | ||||||
|  |         `Embed footer text cannot exceed ${Embed.MAX_FOOTER_TEXT_LENGTH}.` | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
|   | 
 | ||||||
|     if (this.author?.name?.length !== undefined && this.author?.name?.length > Embed.MAX_AUTHOR_NAME_LENGTH) { |     if ( | ||||||
|  |       this.author?.name?.length !== undefined && | ||||||
|  |       this.author?.name?.length > Embed.MAX_AUTHOR_NAME_LENGTH | ||||||
|  |     ) { | ||||||
|       total += Number(this.author?.name?.length) |       total += Number(this.author?.name?.length) | ||||||
|       throw new Error(`Embed author name cannot exceed ${Embed.MAX_AUTHOR_NAME_LENGTH}.`) |       throw new Error( | ||||||
|  |         `Embed author name cannot exceed ${Embed.MAX_AUTHOR_NAME_LENGTH}.` | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
|   | 
 | ||||||
|     if (total > Embed.MAX_EMBED_LENGTH) throw new Error(`Embed characters cannot exceed ${Embed.MAX_EMBED_LENGTH} characters in total.`) |     if (total > Embed.MAX_EMBED_LENGTH) | ||||||
|      |       throw new Error( | ||||||
|  |         `Embed characters cannot exceed ${Embed.MAX_EMBED_LENGTH} characters in total.` | ||||||
|  |       ) | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|       title: this.title, |       title: this.title, | ||||||
|       type: this.type, |       type: this.type, | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue