Merge pull request #122 from DjDeveloperr/refactor
feat: refactor project structure
This commit is contained in:
		
						commit
						ce843b189b
					
				
					 162 changed files with 3963 additions and 2175 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -109,6 +109,7 @@ yarn.lock | |||
| 
 | ||||
| # PRIVACY XDDDD | ||||
| src/test/config.ts | ||||
| test/config.ts | ||||
| .vscode | ||||
| 
 | ||||
| # macOS is shit xD | ||||
|  | @ -117,4 +118,4 @@ src/test/config.ts | |||
| # Webstorm dont forget this duude :) | ||||
| .idea/ | ||||
| 
 | ||||
| src/test/music.mp3 | ||||
| src/test/music.mp3 | ||||
							
								
								
									
										42
									
								
								deploy.ts
									
										
									
									
									
								
							
							
						
						
									
										42
									
								
								deploy.ts
									
										
									
									
									
								
							|  | @ -1,8 +1,9 @@ | |||
| import { | ||||
|   SlashCommandsManager, | ||||
|   SlashClient, | ||||
|   SlashCommandHandlerCallback | ||||
| } from './src/models/slashClient.ts' | ||||
|   SlashCommandHandlerCallback, | ||||
|   SlashCommandHandler | ||||
| } from './src/interactions/mod.ts' | ||||
| import { InteractionResponseType, InteractionType } from './src/types/slash.ts' | ||||
| 
 | ||||
| export interface DeploySlashInitOptions { | ||||
|  | @ -41,7 +42,7 @@ export function init(options: DeploySlashInitOptions): void { | |||
|     try { | ||||
|       const d = await client.verifyFetchEvent({ | ||||
|         respondWith: (...args: any[]) => evt.respondWith(...args), | ||||
|         request: evt.request, | ||||
|         request: evt.request | ||||
|       }) | ||||
|       if (d === false) { | ||||
|         await evt.respondWith( | ||||
|  | @ -60,7 +61,6 @@ export function init(options: DeploySlashInitOptions): void { | |||
| 
 | ||||
|       await (client as any)._process(d) | ||||
|     } catch (e) { | ||||
|       console.log(e) | ||||
|       await client.emit('interactionError', e) | ||||
|     } | ||||
|   } | ||||
|  | @ -69,38 +69,14 @@ export function init(options: DeploySlashInitOptions): void { | |||
| } | ||||
| 
 | ||||
| export function handle( | ||||
|   cmd: | ||||
|     | string | ||||
|     | { | ||||
|       name: string | ||||
|       parent?: string | ||||
|       group?: string | ||||
|       guild?: string | ||||
|     }, | ||||
|   handler: SlashCommandHandlerCallback | ||||
|   cmd: string | SlashCommandHandler, | ||||
|   handler?: SlashCommandHandlerCallback | ||||
| ): void { | ||||
|   const handle = { | ||||
|     name: typeof cmd === 'string' ? cmd : cmd.name, | ||||
|     handler, | ||||
|     ...(typeof cmd === 'string' ? {} : cmd) | ||||
|   } | ||||
| 
 | ||||
|   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 | ||||
|   } | ||||
| 
 | ||||
|   client.handle(handle) | ||||
|   client.handle(cmd, handler) | ||||
| } | ||||
| 
 | ||||
| export { commands, client } | ||||
| export * from './src/types/slash.ts' | ||||
| export * from './src/structures/slash.ts' | ||||
| export * from './src/models/slashClient.ts' | ||||
| export * from './src/interactions/mod.ts' | ||||
| export * from './src/types/channel.ts' | ||||
|  |  | |||
							
								
								
									
										67
									
								
								mod.ts
									
										
									
									
									
								
							
							
						
						
									
										67
									
								
								mod.ts
									
										
									
									
									
								
							|  | @ -1,20 +1,18 @@ | |||
| export { GatewayIntents } from './src/types/gateway.ts' | ||||
| export { Base } from './src/structures/base.ts' | ||||
| export { Gateway } from './src/gateway/index.ts' | ||||
| export type { GatewayTypedEvents } from './src/gateway/index.ts' | ||||
| export type { ClientEvents } from './src/gateway/handlers/index.ts' | ||||
| export * from './src/models/client.ts' | ||||
| export * from './src/models/slashClient.ts' | ||||
| export { Gateway } from './src/gateway/mod.ts' | ||||
| export type { GatewayTypedEvents } from './src/gateway/mod.ts' | ||||
| export type { ClientEvents } from './src/gateway/handlers/mod.ts' | ||||
| export * from './src/client/mod.ts' | ||||
| export * from './src/interactions/mod.ts' | ||||
| export { | ||||
|   RESTManager, | ||||
|   TokenType, | ||||
|   HttpResponseCode, | ||||
|   DiscordAPIError | ||||
| } from './src/models/rest.ts' | ||||
| export type { APIMap, DiscordAPIErrorPayload } from './src/models/rest.ts' | ||||
| export type { RequestHeaders } from './src/models/rest.ts' | ||||
| export type { RESTOptions } from './src/models/rest.ts' | ||||
| export * from './src/models/cacheAdapter.ts' | ||||
| } from './src/rest/mod.ts' | ||||
| export * from './src/rest/mod.ts' | ||||
| export * from './src/cache/adapter.ts' | ||||
| export { | ||||
|   Command, | ||||
|   CommandBuilder, | ||||
|  | @ -22,16 +20,16 @@ export { | |||
|   CommandsManager, | ||||
|   CategoriesManager, | ||||
|   CommandsLoader | ||||
| } from './src/models/command.ts' | ||||
| export type { CommandContext, CommandOptions } from './src/models/command.ts' | ||||
| } from './src/commands/command.ts' | ||||
| export type { CommandContext, CommandOptions } from './src/commands/command.ts' | ||||
| export { | ||||
|   Extension, | ||||
|   ExtensionCommands, | ||||
|   ExtensionsManager | ||||
| } from './src/models/extensions.ts' | ||||
| export { SlashModule } from './src/models/slashModule.ts' | ||||
| export { CommandClient, command } from './src/models/commandClient.ts' | ||||
| export type { CommandClientOptions } from './src/models/commandClient.ts' | ||||
| } from './src/commands/extension.ts' | ||||
| export { SlashModule } from './src/interactions/slashModule.ts' | ||||
| export { CommandClient, command } from './src/commands/client.ts' | ||||
| export type { CommandClientOptions } from './src/commands/client.ts' | ||||
| export { BaseManager } from './src/managers/base.ts' | ||||
| export { BaseChildManager } from './src/managers/baseChild.ts' | ||||
| export { ChannelsManager } from './src/managers/channels.ts' | ||||
|  | @ -51,7 +49,7 @@ export { RolesManager } from './src/managers/roles.ts' | |||
| export { UsersManager } from './src/managers/users.ts' | ||||
| export { InviteManager } from './src/managers/invites.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 type { EditOverwriteOptions } from './src/structures/channel.ts' | ||||
| export { DMChannel } from './src/structures/dmChannel.ts' | ||||
|  | @ -98,7 +96,7 @@ export { Intents } from './src/utils/intents.ts' | |||
| export * from './src/utils/permissions.ts' | ||||
| export { UserFlagsManager } from './src/utils/userFlags.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 type { | ||||
|   ActivityGame, | ||||
|  | @ -106,7 +104,15 @@ export type { | |||
|   ClientStatus, | ||||
|   StatusType | ||||
| } 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 { ImageFormats, ImageSize } from './src/types/cdn.ts' | ||||
| export type { | ||||
|  | @ -131,8 +137,7 @@ export type { | |||
|   MessageStickerPayload, | ||||
|   MessageTypes, | ||||
|   OverwriteAsArg, | ||||
|   Overwrite, | ||||
|   OverwriteAsOptions | ||||
|   Overwrite | ||||
| } from './src/types/channel.ts' | ||||
| export type { EmojiPayload } from './src/types/emoji.ts' | ||||
| export { Verification } from './src/types/guild.ts' | ||||
|  | @ -165,8 +170,24 @@ export type { UserPayload } from './src/types/user.ts' | |||
| export { UserFlags } from './src/types/userFlags.ts' | ||||
| export type { VoiceStatePayload } from './src/types/voice.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/models/redisCache.ts' | ||||
| export * from './src/cache/redis.ts' | ||||
| export { ColorUtil } from './src/utils/colorutil.ts' | ||||
| export type { Colors } from './src/utils/colorutil.ts' | ||||
| export { StoreChannel } from './src/structures/guildStoreChannel.ts' | ||||
| export { StageVoiceChannel } from './src/structures/guildStageVoiceChannel.ts' | ||||
| export { | ||||
|   isCategoryChannel, | ||||
|   isDMChannel, | ||||
|   isGroupDMChannel, | ||||
|   isGuildBasedTextChannel, | ||||
|   isGuildChannel, | ||||
|   isGuildTextChannel, | ||||
|   isNewsChannel, | ||||
|   isStageVoiceChannel, | ||||
|   isStoreChannel, | ||||
|   isTextChannel, | ||||
|   isVoiceChannel, | ||||
|   default as getChannelByType | ||||
| } from './src/utils/channel.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 | ||||
| } | ||||
							
								
								
									
										122
									
								
								src/models/cacheAdapter.ts → src/cache/default.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										122
									
								
								src/models/cacheAdapter.ts → src/cache/default.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,72 +1,50 @@ | |||
| import { Collection } from '../utils/collection.ts' | ||||
| 
 | ||||
| /** | ||||
|  * 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 | ||||
| } | ||||
| 
 | ||||
| /** 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] | ||||
|   } | ||||
| } | ||||
| 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'
 | ||||
							
								
								
									
										11
									
								
								src/models/redisCache.ts → src/cache/redis.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								src/models/redisCache.ts → src/cache/redis.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,5 +1,10 @@ | |||
| import { ICacheAdapter } from './cacheAdapter.ts' | ||||
| import { connect, Redis, RedisConnectOptions } from 'https://deno.land/x/redis@v0.14.1/mod.ts' | ||||
| import { ICacheAdapter } from './adapter.ts' | ||||
| // Not in deps.ts to allow optional dep loading
 | ||||
| import { | ||||
|   connect, | ||||
|   Redis, | ||||
|   RedisConnectOptions | ||||
| } from 'https://deno.land/x/redis@v0.14.1/mod.ts' | ||||
| 
 | ||||
| /** Redis Cache Adapter for using Redis as a cache-provider. */ | ||||
| export class RedisCacheAdapter implements ICacheAdapter { | ||||
|  | @ -102,4 +107,4 @@ export class RedisCacheAdapter implements ICacheAdapter { | |||
|     await this._checkReady() | ||||
|     return (await this.redis?.del(cacheName)) !== 0 | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | @ -1,437 +1,437 @@ | |||
| /* eslint-disable @typescript-eslint/method-signature-style */ | ||||
| import { User } from '../structures/user.ts' | ||||
| import { GatewayIntents } from '../types/gateway.ts' | ||||
| import { Gateway } from '../gateway/index.ts' | ||||
| import { RESTManager, RESTOptions, TokenType } from './rest.ts' | ||||
| import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' | ||||
| import { UsersManager } from '../managers/users.ts' | ||||
| import { GuildManager } from '../managers/guilds.ts' | ||||
| import { ChannelsManager } from '../managers/channels.ts' | ||||
| import { ClientPresence } from '../structures/presence.ts' | ||||
| import { EmojisManager } from '../managers/emojis.ts' | ||||
| import { ActivityGame, ClientActivity } from '../types/presence.ts' | ||||
| import { Extension } from './extensions.ts' | ||||
| import { SlashClient } from './slashClient.ts' | ||||
| import { Interaction } from '../structures/slash.ts' | ||||
| import { ShardManager } from './shard.ts' | ||||
| import { Application } from '../structures/application.ts' | ||||
| import { Invite } from '../structures/invite.ts' | ||||
| import { INVITE } from '../types/endpoint.ts' | ||||
| import { ClientEvents } from '../gateway/handlers/index.ts' | ||||
| import type { Collector } from './collectors.ts' | ||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| import { VoiceRegion } from '../types/voice.ts' | ||||
| import { fetchAuto } from '../../deps.ts' | ||||
| import { DMChannel } from '../structures/dmChannel.ts' | ||||
| import { Template } from '../structures/template.ts' | ||||
| 
 | ||||
| /** OS related properties sent with Gateway Identify */ | ||||
| export interface ClientProperties { | ||||
|   os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string | ||||
|   browser?: 'harmony' | string | ||||
|   device?: 'harmony' | string | ||||
| } | ||||
| 
 | ||||
| /** Some Client Options to modify behaviour */ | ||||
| export interface ClientOptions { | ||||
|   /** ID of the Client/Application to initialize Slash Client REST */ | ||||
|   id?: string | ||||
|   /** Token of the Bot/User */ | ||||
|   token?: string | ||||
|   /** Gateway Intents */ | ||||
|   intents?: GatewayIntents[] | ||||
|   /** Cache Adapter to use, defaults to Collections one */ | ||||
|   cache?: ICacheAdapter | ||||
|   /** Force New Session and don't use cached Session (by persistent caching) */ | ||||
|   forceNewSession?: boolean | ||||
|   /** Startup presence of client */ | ||||
|   presence?: ClientPresence | ClientActivity | ActivityGame | ||||
|   /** Force all requests to Canary API */ | ||||
|   canary?: boolean | ||||
|   /** Time till which Messages are to be cached, in MS. Default is 3600000 */ | ||||
|   messageCacheLifetime?: number | ||||
|   /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ | ||||
|   reactionCacheLifetime?: number | ||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||
|   fetchUncachedReactions?: boolean | ||||
|   /** Client Properties */ | ||||
|   clientProperties?: ClientProperties | ||||
|   /** Enable/Disable Slash Commands Integration (enabled by default) */ | ||||
|   enableSlash?: boolean | ||||
|   /** Disable taking token from env if not provided (token is taken from env if present by default) */ | ||||
|   disableEnvToken?: boolean | ||||
|   /** Override REST Options */ | ||||
|   restOptions?: RESTOptions | ||||
|   /** Whether to fetch Gateway info or not */ | ||||
|   fetchGatewayInfo?: boolean | ||||
|   /** ADVANCED: Shard ID to launch on */ | ||||
|   shard?: number | ||||
|   /** ADVACNED: Shard count. */ | ||||
|   shardCount?: number | 'auto' | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Discord Client. | ||||
|  */ | ||||
| export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||
|   /** REST Manager - used to make all requests */ | ||||
|   rest: RESTManager | ||||
|   /** User which Client logs in to, undefined until logs in */ | ||||
|   user?: User | ||||
|   /** WebSocket ping of Client */ | ||||
|   ping = 0 | ||||
|   /** Token of the Bot/User */ | ||||
|   token?: string | ||||
|   /** Cache Adapter */ | ||||
|   cache: ICacheAdapter = new DefaultCacheAdapter() | ||||
|   /** Gateway Intents */ | ||||
|   intents?: GatewayIntents[] | ||||
|   /** Whether to force new session or not */ | ||||
|   forceNewSession?: boolean | ||||
|   /** Time till messages to stay cached, in MS. */ | ||||
|   messageCacheLifetime: number = 3600000 | ||||
|   /** Time till messages to stay cached, in MS. */ | ||||
|   reactionCacheLifetime: number = 3600000 | ||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||
|   fetchUncachedReactions: boolean = false | ||||
|   /** Client Properties */ | ||||
|   clientProperties: ClientProperties | ||||
|   /** Slash-Commands Management client */ | ||||
|   slash: SlashClient | ||||
|   /** Whether to fetch Gateway info or not */ | ||||
|   fetchGatewayInfo: boolean = true | ||||
| 
 | ||||
|   /** Users Manager, containing all Users cached */ | ||||
|   users: UsersManager = new UsersManager(this) | ||||
|   /** Guilds Manager, providing cache & API interface to Guilds */ | ||||
|   guilds: GuildManager = new GuildManager(this) | ||||
|   /** Channels Manager, providing cache interface to Channels */ | ||||
|   channels: ChannelsManager = new ChannelsManager(this) | ||||
|   /** Channels Manager, providing cache interface to Channels */ | ||||
|   emojis: EmojisManager = new EmojisManager(this) | ||||
| 
 | ||||
|   /** Last READY timestamp */ | ||||
|   upSince?: Date | ||||
| 
 | ||||
|   /** Client's presence. Startup one if set before connecting */ | ||||
|   presence: ClientPresence = new ClientPresence() | ||||
|   _decoratedEvents?: { | ||||
|     [name: string]: (...args: any[]) => void | ||||
|   } | ||||
| 
 | ||||
|   _decoratedSlash?: Array<{ | ||||
|     name: string | ||||
|     guild?: string | ||||
|     parent?: string | ||||
|     group?: string | ||||
|     handler: (interaction: Interaction) => any | ||||
|   }> | ||||
| 
 | ||||
|   _id?: string | ||||
| 
 | ||||
|   /** Shard on which this Client is */ | ||||
|   shard?: number | ||||
|   /** Shard Count */ | ||||
|   shardCount: number | 'auto' = 'auto' | ||||
|   /** Shard Manager of this Client if Sharded */ | ||||
|   shards: ShardManager | ||||
|   /** Collectors set */ | ||||
|   collectors: Set<Collector> = new Set() | ||||
| 
 | ||||
|   /** Since when is Client online (ready). */ | ||||
|   get uptime(): number { | ||||
|     if (this.upSince === undefined) return 0 | ||||
|     else { | ||||
|       const dif = Date.now() - this.upSince.getTime() | ||||
|       if (dif < 0) return 0 | ||||
|       else return dif | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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 | ||||
| 
 | ||||
|   constructor(options: ClientOptions = {}) { | ||||
|     super() | ||||
|     this._id = options.id | ||||
|     this.token = options.token | ||||
|     this.intents = options.intents | ||||
|     this.shards = new ShardManager(this) | ||||
|     this.forceNewSession = options.forceNewSession | ||||
|     if (options.cache !== undefined) this.cache = options.cache | ||||
|     if (options.presence !== undefined) | ||||
|       this.presence = | ||||
|         options.presence instanceof ClientPresence | ||||
|           ? options.presence | ||||
|           : new ClientPresence(options.presence) | ||||
|     if (options.messageCacheLifetime !== undefined) | ||||
|       this.messageCacheLifetime = options.messageCacheLifetime | ||||
|     if (options.reactionCacheLifetime !== undefined) | ||||
|       this.reactionCacheLifetime = options.reactionCacheLifetime | ||||
|     if (options.fetchUncachedReactions === true) | ||||
|       this.fetchUncachedReactions = true | ||||
| 
 | ||||
|     if ( | ||||
|       this._decoratedEvents !== undefined && | ||||
|       Object.keys(this._decoratedEvents).length !== 0 | ||||
|     ) { | ||||
|       Object.entries(this._decoratedEvents).forEach((entry) => { | ||||
|         this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) | ||||
|       }) | ||||
|       this._decoratedEvents = undefined | ||||
|     } | ||||
| 
 | ||||
|     this.clientProperties = | ||||
|       options.clientProperties === undefined | ||||
|         ? { | ||||
|           os: Deno.build.os, | ||||
|           browser: 'harmony', | ||||
|           device: 'harmony' | ||||
|         } | ||||
|         : options.clientProperties | ||||
| 
 | ||||
|     if (options.shard !== undefined) this.shard = options.shard | ||||
|     if (options.shardCount !== undefined) this.shardCount = options.shardCount | ||||
| 
 | ||||
|     this.fetchGatewayInfo = options.fetchGatewayInfo ?? true | ||||
| 
 | ||||
|     if (this.token === undefined) { | ||||
|       try { | ||||
|         const token = Deno.env.get('DISCORD_TOKEN') | ||||
|         if (token !== undefined) { | ||||
|           this.token = token | ||||
|           this.debug('Info', 'Found token in ENV') | ||||
|         } | ||||
|       } catch (e) { } | ||||
|     } | ||||
| 
 | ||||
|     const restOptions: RESTOptions = { | ||||
|       token: () => this.token, | ||||
|       tokenType: TokenType.Bot, | ||||
|       canary: options.canary, | ||||
|       client: this | ||||
|     } | ||||
| 
 | ||||
|     if (options.restOptions !== undefined) | ||||
|       Object.assign(restOptions, options.restOptions) | ||||
|     this.rest = new RESTManager(restOptions) | ||||
| 
 | ||||
|     this.slash = new SlashClient({ | ||||
|       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. | ||||
|    * Please look into using `cache` option. | ||||
|    */ | ||||
|   setAdapter(adapter: ICacheAdapter): Client { | ||||
|     this.cache = adapter | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Changes Presence of Client */ | ||||
|   setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { | ||||
|     if (presence instanceof ClientPresence) { | ||||
|       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
 | ||||
|     this.emit('debug', `[${tag}] ${msg}`) | ||||
|   } | ||||
| 
 | ||||
|   getEstimatedID(): string { | ||||
|     if (this.user !== undefined) return this.user.id | ||||
|     else if (this.token !== undefined) { | ||||
|       try { | ||||
|         return atob(this.token.split('.')[0]) | ||||
|       } catch (e) { | ||||
|         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() | ||||
|     return new Application(this, app) | ||||
|   } | ||||
| 
 | ||||
|   /** Fetch an Invite */ | ||||
|   async fetchInvite(id: string): Promise<Invite> { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       this.rest | ||||
|         .get(INVITE(id)) | ||||
|         .then((data) => { | ||||
|           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. | ||||
|    */ | ||||
|   async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> { | ||||
|     token ??= this.token | ||||
|     if (token === undefined) throw new Error('No Token Provided') | ||||
|     this.token = token | ||||
|     if (intents !== undefined && this.intents !== undefined) { | ||||
|       this.debug( | ||||
|         'client', | ||||
|         'Intents were set in both client and connect function. Using the one in the connect function...' | ||||
|       ) | ||||
|     } else if (intents === undefined && this.intents !== undefined) { | ||||
|       intents = this.intents | ||||
|     } else if (intents !== undefined && this.intents === undefined) { | ||||
|       this.intents = intents | ||||
|     } else throw new Error('No Gateway Intents were provided') | ||||
| 
 | ||||
|     this.rest.token = token | ||||
|     if (this.shard !== undefined) { | ||||
|       if (typeof this.shardCount === 'number') | ||||
|         this.shards.cachedShardCount = this.shardCount | ||||
|       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 | ||||
|     this.gateway.sequenceID = undefined | ||||
|     this.gateway.sessionID = undefined | ||||
|     await this.gateway.cache.delete('seq') | ||||
|     await this.gateway.cache.delete('session_id') | ||||
|     this.gateway.close() | ||||
|     this.user = undefined | ||||
|     this.upSince = undefined | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Attempt to Close current Gateway connection and Resume */ | ||||
|   async reconnect(): Promise<Client> { | ||||
|     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 | ||||
|     else { | ||||
|       this.collectors.add(collector) | ||||
|       return true | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** Remove a Collector */ | ||||
|   removeCollector(collector: Collector): boolean { | ||||
|     if (!this.collectors.has(collector)) return false | ||||
|     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()) { | ||||
|       if (collector.event === event) collectors.push(collector) | ||||
|     } | ||||
|     if (collectors.length !== 0) { | ||||
|       this.collectors.forEach((collector) => collector._fire(...args)) | ||||
|     } | ||||
|     // TODO(DjDeveloperr): Fix this ts-ignore
 | ||||
|     // 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() | ||||
|   } | ||||
| 
 | ||||
|   /** Modify current (Client) User. */ | ||||
|   async editUser(data: { | ||||
|     username?: string | ||||
|     avatar?: string | ||||
|   }): Promise<Client> { | ||||
|     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) | ||||
|     } | ||||
| 
 | ||||
|     await this.rest.api.users['@me'].patch({ | ||||
|       username: data.username, | ||||
|       avatar: data.avatar | ||||
|     }) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** 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 }) | ||||
|   } | ||||
| 
 | ||||
|   /** Create a DM Channel with a User */ | ||||
|   async createDM(user: User | string): Promise<DMChannel> { | ||||
|     const id = typeof user === 'object' ? user.id : user | ||||
|     const dmPayload = await this.rest.api.users['@me'].channels.post({ | ||||
|       recipient_id: id | ||||
|     }) | ||||
|     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() | ||||
|     return new Template(this, payload) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** Event decorator to create an Event handler from function */ | ||||
| export function event(name?: keyof ClientEvents) { | ||||
|   return function ( | ||||
|     client: Client | Extension, | ||||
|     prop: keyof ClientEvents | string | ||||
|   ) { | ||||
|     const listener = ((client as unknown) as { | ||||
|       [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any | ||||
|     })[(prop as unknown) as keyof ClientEvents] | ||||
|     if (typeof listener !== 'function') | ||||
|       throw new Error('@event decorator requires a function') | ||||
|     if (client._decoratedEvents === undefined) client._decoratedEvents = {} | ||||
|     const key = name === undefined ? prop : name | ||||
| 
 | ||||
|     client._decoratedEvents[key] = listener | ||||
|   } | ||||
| } | ||||
| /* eslint-disable @typescript-eslint/method-signature-style */ | ||||
| import type { User } from '../structures/user.ts' | ||||
| import { GatewayIntents } from '../types/gateway.ts' | ||||
| import { Gateway } from '../gateway/mod.ts' | ||||
| import { RESTManager, RESTOptions, TokenType } from '../rest/mod.ts' | ||||
| import { DefaultCacheAdapter, ICacheAdapter } from '../cache/mod.ts' | ||||
| import { UsersManager } from '../managers/users.ts' | ||||
| import { GuildManager } from '../managers/guilds.ts' | ||||
| import { ChannelsManager } from '../managers/channels.ts' | ||||
| import { ClientPresence } from '../structures/presence.ts' | ||||
| import { EmojisManager } from '../managers/emojis.ts' | ||||
| import { ActivityGame, ClientActivity } from '../types/presence.ts' | ||||
| import type { Extension } from '../commands/extension.ts' | ||||
| import { SlashClient } from '../interactions/slashClient.ts' | ||||
| import type { Interaction } from '../structures/slash.ts' | ||||
| import { ShardManager } from './shard.ts' | ||||
| import { Application } from '../structures/application.ts' | ||||
| import { Invite } from '../structures/invite.ts' | ||||
| import { INVITE } from '../types/endpoint.ts' | ||||
| import type { ClientEvents } from '../gateway/handlers/mod.ts' | ||||
| import type { Collector } from './collectors.ts' | ||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| import type { VoiceRegion } from '../types/voice.ts' | ||||
| import { fetchAuto } from '../../deps.ts' | ||||
| import type { DMChannel } from '../structures/dmChannel.ts' | ||||
| import { Template } from '../structures/template.ts' | ||||
| 
 | ||||
| /** OS related properties sent with Gateway Identify */ | ||||
| export interface ClientProperties { | ||||
|   os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string | ||||
|   browser?: 'harmony' | string | ||||
|   device?: 'harmony' | string | ||||
| } | ||||
| 
 | ||||
| /** Some Client Options to modify behaviour */ | ||||
| export interface ClientOptions { | ||||
|   /** ID of the Client/Application to initialize Slash Client REST */ | ||||
|   id?: string | ||||
|   /** Token of the Bot/User */ | ||||
|   token?: string | ||||
|   /** Gateway Intents */ | ||||
|   intents?: GatewayIntents[] | ||||
|   /** Cache Adapter to use, defaults to Collections one */ | ||||
|   cache?: ICacheAdapter | ||||
|   /** Force New Session and don't use cached Session (by persistent caching) */ | ||||
|   forceNewSession?: boolean | ||||
|   /** Startup presence of client */ | ||||
|   presence?: ClientPresence | ClientActivity | ActivityGame | ||||
|   /** Force all requests to Canary API */ | ||||
|   canary?: boolean | ||||
|   /** Time till which Messages are to be cached, in MS. Default is 3600000 */ | ||||
|   messageCacheLifetime?: number | ||||
|   /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ | ||||
|   reactionCacheLifetime?: number | ||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||
|   fetchUncachedReactions?: boolean | ||||
|   /** Client Properties */ | ||||
|   clientProperties?: ClientProperties | ||||
|   /** Enable/Disable Slash Commands Integration (enabled by default) */ | ||||
|   enableSlash?: boolean | ||||
|   /** Disable taking token from env if not provided (token is taken from env if present by default) */ | ||||
|   disableEnvToken?: boolean | ||||
|   /** Override REST Options */ | ||||
|   restOptions?: RESTOptions | ||||
|   /** Whether to fetch Gateway info or not */ | ||||
|   fetchGatewayInfo?: boolean | ||||
|   /** ADVANCED: Shard ID to launch on */ | ||||
|   shard?: number | ||||
|   /** ADVACNED: Shard count. */ | ||||
|   shardCount?: number | 'auto' | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Discord Client. | ||||
|  */ | ||||
| export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||
|   /** REST Manager - used to make all requests */ | ||||
|   rest: RESTManager | ||||
|   /** User which Client logs in to, undefined until logs in */ | ||||
|   user?: User | ||||
|   /** WebSocket ping of Client */ | ||||
|   ping = 0 | ||||
|   /** Token of the Bot/User */ | ||||
|   token?: string | ||||
|   /** Cache Adapter */ | ||||
|   cache: ICacheAdapter = new DefaultCacheAdapter() | ||||
|   /** Gateway Intents */ | ||||
|   intents?: GatewayIntents[] | ||||
|   /** Whether to force new session or not */ | ||||
|   forceNewSession?: boolean | ||||
|   /** Time till messages to stay cached, in MS. */ | ||||
|   messageCacheLifetime: number = 3600000 | ||||
|   /** Time till messages to stay cached, in MS. */ | ||||
|   reactionCacheLifetime: number = 3600000 | ||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||
|   fetchUncachedReactions: boolean = false | ||||
|   /** Client Properties */ | ||||
|   clientProperties: ClientProperties | ||||
|   /** Slash-Commands Management client */ | ||||
|   slash: SlashClient | ||||
|   /** Whether to fetch Gateway info or not */ | ||||
|   fetchGatewayInfo: boolean = true | ||||
| 
 | ||||
|   /** Users Manager, containing all Users cached */ | ||||
|   users: UsersManager = new UsersManager(this) | ||||
|   /** Guilds Manager, providing cache & API interface to Guilds */ | ||||
|   guilds: GuildManager = new GuildManager(this) | ||||
|   /** Channels Manager, providing cache interface to Channels */ | ||||
|   channels: ChannelsManager = new ChannelsManager(this) | ||||
|   /** Channels Manager, providing cache interface to Channels */ | ||||
|   emojis: EmojisManager = new EmojisManager(this) | ||||
| 
 | ||||
|   /** Last READY timestamp */ | ||||
|   upSince?: Date | ||||
| 
 | ||||
|   /** Client's presence. Startup one if set before connecting */ | ||||
|   presence: ClientPresence = new ClientPresence() | ||||
|   _decoratedEvents?: { | ||||
|     [name: string]: (...args: any[]) => void | ||||
|   } | ||||
| 
 | ||||
|   _decoratedSlash?: Array<{ | ||||
|     name: string | ||||
|     guild?: string | ||||
|     parent?: string | ||||
|     group?: string | ||||
|     handler: (interaction: Interaction) => any | ||||
|   }> | ||||
| 
 | ||||
|   _id?: string | ||||
| 
 | ||||
|   /** Shard on which this Client is */ | ||||
|   shard?: number | ||||
|   /** Shard Count */ | ||||
|   shardCount: number | 'auto' = 'auto' | ||||
|   /** Shard Manager of this Client if Sharded */ | ||||
|   shards: ShardManager | ||||
|   /** Collectors set */ | ||||
|   collectors: Set<Collector> = new Set() | ||||
| 
 | ||||
|   /** Since when is Client online (ready). */ | ||||
|   get uptime(): number { | ||||
|     if (this.upSince === undefined) return 0 | ||||
|     else { | ||||
|       const dif = Date.now() - this.upSince.getTime() | ||||
|       if (dif < 0) return 0 | ||||
|       else return dif | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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 | ||||
| 
 | ||||
|   constructor(options: ClientOptions = {}) { | ||||
|     super() | ||||
|     this._id = options.id | ||||
|     this.token = options.token | ||||
|     this.intents = options.intents | ||||
|     this.shards = new ShardManager(this) | ||||
|     this.forceNewSession = options.forceNewSession | ||||
|     if (options.cache !== undefined) this.cache = options.cache | ||||
|     if (options.presence !== undefined) | ||||
|       this.presence = | ||||
|         options.presence instanceof ClientPresence | ||||
|           ? options.presence | ||||
|           : new ClientPresence(options.presence) | ||||
|     if (options.messageCacheLifetime !== undefined) | ||||
|       this.messageCacheLifetime = options.messageCacheLifetime | ||||
|     if (options.reactionCacheLifetime !== undefined) | ||||
|       this.reactionCacheLifetime = options.reactionCacheLifetime | ||||
|     if (options.fetchUncachedReactions === true) | ||||
|       this.fetchUncachedReactions = true | ||||
| 
 | ||||
|     if ( | ||||
|       this._decoratedEvents !== undefined && | ||||
|       Object.keys(this._decoratedEvents).length !== 0 | ||||
|     ) { | ||||
|       Object.entries(this._decoratedEvents).forEach((entry) => { | ||||
|         this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) | ||||
|       }) | ||||
|       this._decoratedEvents = undefined | ||||
|     } | ||||
| 
 | ||||
|     this.clientProperties = | ||||
|       options.clientProperties === undefined | ||||
|         ? { | ||||
|             os: Deno.build.os, | ||||
|             browser: 'harmony', | ||||
|             device: 'harmony' | ||||
|           } | ||||
|         : options.clientProperties | ||||
| 
 | ||||
|     if (options.shard !== undefined) this.shard = options.shard | ||||
|     if (options.shardCount !== undefined) this.shardCount = options.shardCount | ||||
| 
 | ||||
|     this.fetchGatewayInfo = options.fetchGatewayInfo ?? true | ||||
| 
 | ||||
|     if (this.token === undefined) { | ||||
|       try { | ||||
|         const token = Deno.env.get('DISCORD_TOKEN') | ||||
|         if (token !== undefined) { | ||||
|           this.token = token | ||||
|           this.debug('Info', 'Found token in ENV') | ||||
|         } | ||||
|       } catch (e) {} | ||||
|     } | ||||
| 
 | ||||
|     const restOptions: RESTOptions = { | ||||
|       token: () => this.token, | ||||
|       tokenType: TokenType.Bot, | ||||
|       canary: options.canary, | ||||
|       client: this | ||||
|     } | ||||
| 
 | ||||
|     if (options.restOptions !== undefined) | ||||
|       Object.assign(restOptions, options.restOptions) | ||||
|     this.rest = new RESTManager(restOptions) | ||||
| 
 | ||||
|     this.slash = new SlashClient({ | ||||
|       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. | ||||
|    * Please look into using `cache` option. | ||||
|    */ | ||||
|   setAdapter(adapter: ICacheAdapter): Client { | ||||
|     this.cache = adapter | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Changes Presence of Client */ | ||||
|   setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { | ||||
|     if (presence instanceof ClientPresence) { | ||||
|       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
 | ||||
|     this.emit('debug', `[${tag}] ${msg}`) | ||||
|   } | ||||
| 
 | ||||
|   getEstimatedID(): string { | ||||
|     if (this.user !== undefined) return this.user.id | ||||
|     else if (this.token !== undefined) { | ||||
|       try { | ||||
|         return atob(this.token.split('.')[0]) | ||||
|       } catch (e) { | ||||
|         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() | ||||
|     return new Application(this, app) | ||||
|   } | ||||
| 
 | ||||
|   /** Fetch an Invite */ | ||||
|   async fetchInvite(id: string): Promise<Invite> { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       this.rest | ||||
|         .get(INVITE(id)) | ||||
|         .then((data) => { | ||||
|           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. | ||||
|    */ | ||||
|   async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> { | ||||
|     token ??= this.token | ||||
|     if (token === undefined) throw new Error('No Token Provided') | ||||
|     this.token = token | ||||
|     if (intents !== undefined && this.intents !== undefined) { | ||||
|       this.debug( | ||||
|         'client', | ||||
|         'Intents were set in both client and connect function. Using the one in the connect function...' | ||||
|       ) | ||||
|     } else if (intents === undefined && this.intents !== undefined) { | ||||
|       intents = this.intents | ||||
|     } else if (intents !== undefined && this.intents === undefined) { | ||||
|       this.intents = intents | ||||
|     } else throw new Error('No Gateway Intents were provided') | ||||
| 
 | ||||
|     this.rest.token = token | ||||
|     if (this.shard !== undefined) { | ||||
|       if (typeof this.shardCount === 'number') | ||||
|         this.shards.cachedShardCount = this.shardCount | ||||
|       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 | ||||
|     this.gateway.sequenceID = undefined | ||||
|     this.gateway.sessionID = undefined | ||||
|     await this.gateway.cache.delete('seq') | ||||
|     await this.gateway.cache.delete('session_id') | ||||
|     this.gateway.close() | ||||
|     this.user = undefined | ||||
|     this.upSince = undefined | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Attempt to Close current Gateway connection and Resume */ | ||||
|   async reconnect(): Promise<Client> { | ||||
|     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 | ||||
|     else { | ||||
|       this.collectors.add(collector) | ||||
|       return true | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** Remove a Collector */ | ||||
|   removeCollector(collector: Collector): boolean { | ||||
|     if (!this.collectors.has(collector)) return false | ||||
|     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()) { | ||||
|       if (collector.event === event) collectors.push(collector) | ||||
|     } | ||||
|     if (collectors.length !== 0) { | ||||
|       this.collectors.forEach((collector) => collector._fire(...args)) | ||||
|     } | ||||
|     // TODO(DjDeveloperr): Fix this ts-ignore
 | ||||
|     // 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() | ||||
|   } | ||||
| 
 | ||||
|   /** Modify current (Client) User. */ | ||||
|   async editUser(data: { | ||||
|     username?: string | ||||
|     avatar?: string | ||||
|   }): Promise<Client> { | ||||
|     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) | ||||
|     } | ||||
| 
 | ||||
|     await this.rest.api.users['@me'].patch({ | ||||
|       username: data.username, | ||||
|       avatar: data.avatar | ||||
|     }) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** 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 }) | ||||
|   } | ||||
| 
 | ||||
|   /** Create a DM Channel with a User */ | ||||
|   async createDM(user: User | string): Promise<DMChannel> { | ||||
|     const id = typeof user === 'object' ? user.id : user | ||||
|     const dmPayload = await this.rest.api.users['@me'].channels.post({ | ||||
|       recipient_id: id | ||||
|     }) | ||||
|     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() | ||||
|     return new Template(this, payload) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** Event decorator to create an Event handler from function */ | ||||
| export function event(name?: keyof ClientEvents) { | ||||
|   return function ( | ||||
|     client: Client | Extension, | ||||
|     prop: keyof ClientEvents | string | ||||
|   ) { | ||||
|     const listener = ((client as unknown) as { | ||||
|       [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any | ||||
|     })[(prop as unknown) as keyof ClientEvents] | ||||
|     if (typeof listener !== 'function') | ||||
|       throw new Error('@event decorator requires a function') | ||||
|     if (client._decoratedEvents === undefined) client._decoratedEvents = {} | ||||
|     const key = name === undefined ? prop : name | ||||
| 
 | ||||
|     client._decoratedEvents[key] = listener | ||||
|   } | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| 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' | ||||
| 
 | ||||
| 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 type { Client } from './client.ts' | ||||
| import { RESTManager } from './rest.ts' | ||||
| import { Gateway } from '../gateway/index.ts' | ||||
| import { RESTManager } from '../rest/mod.ts' | ||||
| import { Gateway } from '../gateway/mod.ts' | ||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| import { GatewayEvents } from '../types/gateway.ts' | ||||
| import { delay } from '../utils/delay.ts' | ||||
|  | @ -61,10 +61,24 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> { | |||
|     let shardCount: number | ||||
|     if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount | ||||
|     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() | ||||
|         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 | ||||
|       } else shardCount = this.client.shardCount ?? 1 | ||||
|       } else | ||||
|         shardCount = | ||||
|           typeof this.client.shardCount === 'string' | ||||
|             ? 1 | ||||
|             : this.client.shardCount ?? 1 | ||||
|     } | ||||
|     this.cachedShardCount = shardCount | ||||
|     return this.cachedShardCount | ||||
|  | @ -1,6 +1,6 @@ | |||
| import { Message } from '../structures/message.ts' | ||||
| import { GuildTextBasedChannel } from '../structures/guildTextChannel.ts' | ||||
| import { Client, ClientOptions } from './client.ts' | ||||
| import type { Message } from '../structures/message.ts' | ||||
| import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts' | ||||
| import { Client, ClientOptions } from '../client/mod.ts' | ||||
| import { | ||||
|   CategoriesManager, | ||||
|   Command, | ||||
|  | @ -9,7 +9,7 @@ import { | |||
|   CommandsManager, | ||||
|   parseCommand | ||||
| } from './command.ts' | ||||
| import { Extension, ExtensionsManager } from './extensions.ts' | ||||
| import { Extension, ExtensionsManager } from './extension.ts' | ||||
| 
 | ||||
| type PrefixReturnType = string | string[] | Promise<string | string[]> | ||||
| 
 | ||||
|  | @ -1,10 +1,10 @@ | |||
| import { Guild } from '../structures/guild.ts' | ||||
| import { Message } from '../structures/message.ts' | ||||
| import { TextChannel } from '../structures/textChannel.ts' | ||||
| import { User } from '../structures/user.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import type { Message } from '../structures/message.ts' | ||||
| import type { TextChannel } from '../structures/textChannel.ts' | ||||
| import type { User } from '../structures/user.ts' | ||||
| import { Collection } from '../utils/collection.ts' | ||||
| import { CommandClient } from './commandClient.ts' | ||||
| import { Extension } from './extensions.ts' | ||||
| import type { CommandClient } from './client.ts' | ||||
| import type { Extension } from './extension.ts' | ||||
| import { join, walk } from '../../deps.ts' | ||||
| 
 | ||||
| export interface CommandContext { | ||||
|  | @ -72,6 +72,8 @@ export interface CommandOptions { | |||
| } | ||||
| 
 | ||||
| export class Command implements CommandOptions { | ||||
|   static meta?: CommandOptions | ||||
| 
 | ||||
|   name: string = '' | ||||
|   description?: string | ||||
|   category?: string | ||||
|  | @ -486,12 +488,16 @@ export class CommandsManager { | |||
| 
 | ||||
|   /** Add a Command */ | ||||
|   add(cmd: Command | typeof Command): boolean { | ||||
|     // eslint-disable-next-line new-cap
 | ||||
|     if (!(cmd instanceof Command)) cmd = new cmd() | ||||
|     if (!(cmd instanceof Command)) { | ||||
|       const CmdClass = cmd | ||||
|       cmd = new CmdClass() | ||||
|       Object.assign(cmd, CmdClass.meta ?? {}) | ||||
|     } | ||||
|     if (this.exists(cmd, cmd.extension?.subPrefix)) | ||||
|       throw new Error( | ||||
|         `Failed to add Command '${cmd.toString()}' with name/alias already exists.` | ||||
|       ) | ||||
|     if (cmd.name === '') throw new Error('Command has no name') | ||||
|     this.list.set( | ||||
|       `${cmd.name}-${ | ||||
|         this.list.filter((e) => | ||||
|  | @ -1,7 +1,7 @@ | |||
| import { ClientEvents } from '../../mod.ts' | ||||
| import { Collection } from '../utils/collection.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 | ||||
| 
 | ||||
							
								
								
									
										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 { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const applicationCommandCreate: GatewayEventHandler = async ( | ||||
|   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 { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const applicationCommandDelete: GatewayEventHandler = async ( | ||||
|   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 { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const applicationCommandUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,7 +1,10 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import getChannelByType from '../../utils/getChannelByType.ts' | ||||
| import { ChannelPayload, GuildChannelPayload } from '../../types/channel.ts' | ||||
| import { Guild } from '../../structures/guild.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import getChannelByType from '../../utils/channel.ts' | ||||
| import type { | ||||
|   ChannelPayload, | ||||
|   GuildChannelPayload | ||||
| } from '../../types/channel.ts' | ||||
| import type { Guild } from '../../structures/guild.ts' | ||||
| 
 | ||||
| export const channelCreate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { ChannelPayload } from '../../types/channel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { ChannelPayload } from '../../types/channel.ts' | ||||
| 
 | ||||
| export const channelDelete: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import { ChannelPinsUpdatePayload } from '../../types/gateway.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { ChannelPinsUpdatePayload } from '../../types/gateway.ts' | ||||
| 
 | ||||
| export const channelPinsUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Channel } from '../../structures/channel.ts' | ||||
| import { ChannelPayload } from '../../types/channel.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Channel } from '../../structures/channel.ts' | ||||
| import type { ChannelPayload } from '../../types/channel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const channelUpdate: GatewayEventHandler = async ( | ||||
|   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 { User } from '../../structures/user.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 { User } from '../../structures/user.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 { GuildPayload } from '../../types/guild.ts' | ||||
| import { GuildChannelPayload } from '../../types/channel.ts' | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Guild } from '../../structures/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 ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import { Emoji } from '../../structures/emoji.ts' | |||
| import { Guild } from '../../structures/guild.ts' | ||||
| import { EmojiPayload } from '../../types/emoji.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 ( | ||||
|   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 { 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 { GuildMemberAddPayload } from '../../types/gateway.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 { User } from '../../structures/user.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 { GuildMemberUpdatePayload } from '../../types/gateway.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 { 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 { GuildRoleCreatePayload } from '../../types/gateway.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 { 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 { GuildRoleUpdatePayload } from '../../types/gateway.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 { GuildPayload } from '../../types/guild.ts' | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | |||
| import { InteractionPayload } from '../../types/slash.ts' | ||||
| import { UserPayload } from '../../types/user.ts' | ||||
| import { Permissions } from '../../utils/permissions.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import { User } from '../../structures/user.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 { InviteCreatePayload } from '../../types/gateway.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 { InviteDeletePayload } from '../../types/gateway.ts' | ||||
| import { PartialInvitePayload } from '../../types/invite.ts' | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| 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 { MessagePayload } from '../../types/channel.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { MessagePayload } from '../../types/channel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const messageCreate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import { MessageDeletePayload } from '../../types/gateway.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { MessageDeletePayload } from '../../types/gateway.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const messageDelete: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Message } from '../../structures/message.ts' | ||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||
| import { MessageDeleteBulkPayload } from '../../types/gateway.ts' | ||||
| import type { Message } from '../../structures/message.ts' | ||||
| import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||
| import type { MessageDeleteBulkPayload } from '../../types/gateway.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 ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { MessageReactionAddPayload } from '../../types/gateway.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import { MessageReaction } from '../../structures/messageReaction.ts' | ||||
| import { UserPayload } from '../../types/user.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { MessageReactionAddPayload } from '../../types/gateway.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { MessageReaction } from '../../structures/messageReaction.ts' | ||||
| import type { UserPayload } from '../../types/user.ts' | ||||
| 
 | ||||
| export const messageReactionAdd: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { MessageReactionRemovePayload } from '../../types/gateway.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { MessageReactionRemovePayload } from '../../types/gateway.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| 
 | ||||
| export const messageReactionRemove: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { MessageReactionRemoveAllPayload } from '../../types/gateway.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { MessageReactionRemoveAllPayload } from '../../types/gateway.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| 
 | ||||
| export const messageReactionRemoveAll: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| 
 | ||||
| export const messageReactionRemoveEmoji: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Message } from '../../structures/message.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import { MessagePayload } from '../../types/channel.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Message } from '../../structures/message.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { MessagePayload } from '../../types/channel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const messageUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { GatewayEventHandler } from '../index.ts' | ||||
| import { | ||||
| import type { GatewayEventHandler } from '../mod.ts' | ||||
| import type { | ||||
|   GatewayEvents, | ||||
|   MessageDeletePayload, | ||||
|   TypingStartGuildData | ||||
|  | @ -31,18 +31,18 @@ import { webhooksUpdate } from './webhooksUpdate.ts' | |||
| import { messageDeleteBulk } from './messageDeleteBulk.ts' | ||||
| import { userUpdate } from './userUpdate.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 { Guild } from '../../structures/guild.ts' | ||||
| import { User } from '../../structures/user.ts' | ||||
| import { Emoji } from '../../structures/emoji.ts' | ||||
| import { Member } from '../../structures/member.ts' | ||||
| import { Role } from '../../structures/role.ts' | ||||
| import { Message } from '../../structures/message.ts' | ||||
| import { Collection } from '../../utils/collection.ts' | ||||
| import type { Guild } from '../../structures/guild.ts' | ||||
| import type { User } from '../../structures/user.ts' | ||||
| import type { Emoji } from '../../structures/emoji.ts' | ||||
| import type { Member } from '../../structures/member.ts' | ||||
| import type { Role } from '../../structures/role.ts' | ||||
| import type { Message } from '../../structures/message.ts' | ||||
| import type { Collection } from '../../utils/collection.ts' | ||||
| import { voiceServerUpdate } from './voiceServerUpdate.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 { messageReactionRemove } from './messageReactionRemove.ts' | ||||
| import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts' | ||||
|  | @ -51,23 +51,23 @@ import { guildMembersChunk } from './guildMembersChunk.ts' | |||
| import { presenceUpdate } from './presenceUpdate.ts' | ||||
| import { inviteCreate } from './inviteCreate.ts' | ||||
| import { inviteDelete } from './inviteDelete.ts' | ||||
| import { MessageReaction } from '../../structures/messageReaction.ts' | ||||
| import { Invite } from '../../structures/invite.ts' | ||||
| import { Presence } from '../../structures/presence.ts' | ||||
| import { | ||||
| import type { MessageReaction } from '../../structures/messageReaction.ts' | ||||
| import type { Invite } from '../../structures/invite.ts' | ||||
| import type { Presence } from '../../structures/presence.ts' | ||||
| import type { | ||||
|   EveryChannelTypes, | ||||
|   EveryTextChannelTypes | ||||
| } from '../../utils/getChannelByType.ts' | ||||
| } from '../../utils/channel.ts' | ||||
| import { interactionCreate } from './interactionCreate.ts' | ||||
| import { Interaction } from '../../structures/slash.ts' | ||||
| import { CommandContext } from '../../models/command.ts' | ||||
| import { RequestMethods } from '../../models/rest.ts' | ||||
| import { PartialInvitePayload } from '../../types/invite.ts' | ||||
| import { GuildChannels } from '../../types/guild.ts' | ||||
| import type { Interaction } from '../../structures/slash.ts' | ||||
| import type { CommandContext } from '../../commands/command.ts' | ||||
| import type { RequestMethods } from '../../rest/types.ts' | ||||
| import type { PartialInvitePayload } from '../../types/invite.ts' | ||||
| import type { GuildChannels } from '../../types/guild.ts' | ||||
| import { applicationCommandCreate } from './applicationCommandCreate.ts' | ||||
| import { applicationCommandDelete } from './applicationCommandDelete.ts' | ||||
| import { applicationCommandUpdate } from './applicationCommandUpdate.ts' | ||||
| import { SlashCommand } from '../../models/slashClient.ts' | ||||
| import type { SlashCommand } from '../../interactions/slashCommand.ts' | ||||
| 
 | ||||
| export const gatewayHandlers: { | ||||
|   [eventCode in GatewayEvents]: GatewayEventHandler | undefined | ||||
|  | @ -393,7 +393,15 @@ export type ClientEvents = { | |||
|     } | ||||
|   ] | ||||
|   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] | ||||
|   voiceStateRemoveUncached: [data: { guild: Guild; member: Member }] | ||||
|   userUpdateUncached: [user: User] | ||||
|  | @ -1,5 +1,5 @@ | |||
| import { PresenceUpdatePayload } from '../../types/gateway.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { PresenceUpdatePayload } from '../../types/gateway.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const presenceUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { User } from '../../structures/user.ts' | ||||
| import { Ready } from '../../types/gateway.ts' | ||||
| import { GuildPayload } from '../../types/guild.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Ready } from '../../types/gateway.ts' | ||||
| import type { GuildPayload } from '../../types/guild.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const ready: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const reconnect: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { User } from '../../structures/user.ts' | ||||
| import { CLIENT_USER } from '../../types/endpoint.ts' | ||||
| import { Resume } from '../../types/gateway.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Resume } from '../../types/gateway.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const resume: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Member } from '../../structures/member.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import { TypingStartPayload } from '../../types/gateway.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { TextChannel } from '../../structures/textChannel.ts' | ||||
| import type { TypingStartPayload } from '../../types/gateway.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| // TODO: Do we need to add uncached events here?
 | ||||
| export const typingStart: GatewayEventHandler = async ( | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { User } from '../../structures/user.ts' | ||||
| import { UserPayload } from '../../types/user.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { User } from '../../structures/user.ts' | ||||
| import type { UserPayload } from '../../types/user.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const userUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Guild } from '../../structures/guild.ts' | ||||
| import { VoiceServerUpdatePayload } from '../../types/gateway.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Guild } from '../../structures/guild.ts' | ||||
| import type { VoiceServerUpdatePayload } from '../../types/gateway.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const voiceServerUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Guild } from '../../structures/guild.ts' | ||||
| import { VoiceState } from '../../structures/voiceState.ts' | ||||
| import { MemberPayload } from '../../types/guild.ts' | ||||
| import { VoiceStatePayload } from '../../types/voice.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import type { Guild } from '../../structures/guild.ts' | ||||
| import type { VoiceState } from '../../structures/voiceState.ts' | ||||
| import type { MemberPayload } from '../../types/guild.ts' | ||||
| import type { VoiceStatePayload } from '../../types/voice.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| 
 | ||||
| export const voiceStateUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import { Guild } from '../../structures/guild.ts' | ||||
| import { WebhooksUpdatePayload } from '../../types/gateway.ts' | ||||
| import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||
| import type { Gateway, GatewayEventHandler } from '../mod.ts' | ||||
| import type { Guild } from '../../structures/guild.ts' | ||||
| import type { WebhooksUpdatePayload } from '../../types/gateway.ts' | ||||
| import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts' | ||||
| 
 | ||||
| export const webhooksUpdate: GatewayEventHandler = async ( | ||||
|   gateway: Gateway, | ||||
|  |  | |||
|  | @ -1,9 +1,5 @@ | |||
| import { unzlib } from '../../deps.ts' | ||||
| import { Client } from '../models/client.ts' | ||||
| import { | ||||
|   DISCORD_GATEWAY_URL, | ||||
|   DISCORD_API_VERSION | ||||
| } from '../consts/urlsAndVersions.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import { GatewayResponse } from '../types/gatewayResponse.ts' | ||||
| import { | ||||
|   GatewayOpcodes, | ||||
|  | @ -12,13 +8,14 @@ import { | |||
|   StatusUpdatePayload, | ||||
|   GatewayEvents | ||||
| } from '../types/gateway.ts' | ||||
| import { gatewayHandlers } from './handlers/index.ts' | ||||
| import { gatewayHandlers } from './handlers/mod.ts' | ||||
| import { GatewayCache } from '../managers/gatewayCache.ts' | ||||
| import { delay } from '../utils/delay.ts' | ||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| import { decodeText } from '../utils/encoding.ts' | ||||
| import { Constants } from '../types/constants.ts' | ||||
| 
 | ||||
| export interface RequestMembersOptions { | ||||
|   limit?: number | ||||
|  | @ -269,21 +266,6 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | |||
|     if (typeof this.client.intents !== 'object') | ||||
|       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) { | ||||
|       const sessionIDCached = await this.cache.get( | ||||
|         `session_id_${this.shards?.join('-') ?? '0'}` | ||||
|  | @ -417,7 +399,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | |||
|     this.debug('Initializing WebSocket...') | ||||
|     this.websocket = new WebSocket( | ||||
|       // 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' | ||||
							
								
								
									
										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' | ||||
|  | @ -1,4 +1,3 @@ | |||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { | ||||
|   Interaction, | ||||
|   InteractionApplicationCommandResolved | ||||
|  | @ -7,358 +6,16 @@ import { | |||
|   InteractionPayload, | ||||
|   InteractionResponsePayload, | ||||
|   InteractionType, | ||||
|   SlashCommandChoice, | ||||
|   SlashCommandOption, | ||||
|   SlashCommandOptionType, | ||||
|   SlashCommandPartial, | ||||
|   SlashCommandPayload | ||||
|   SlashCommandOptionType | ||||
| } from '../types/slash.ts' | ||||
| import { Collection } from '../utils/collection.ts' | ||||
| import type { Client } from './client.ts' | ||||
| import { RESTManager } from './rest.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' | ||||
| 
 | ||||
| export class SlashCommand { | ||||
|   slash: SlashCommandsManager | ||||
|   id: string | ||||
|   applicationID: string | ||||
|   name: string | ||||
|   description: string | ||||
|   options: SlashCommandOption[] | ||||
|   guild?: Guild | ||||
|   _guild?: string | ||||
| 
 | ||||
|   constructor( | ||||
|     manager: SlashCommandsManager, | ||||
|     data: SlashCommandPayload, | ||||
|     guild?: Guild | ||||
|   ) { | ||||
|     this.slash = manager | ||||
|     this.id = data.id | ||||
|     this.applicationID = data.application_id | ||||
|     this.name = data.name | ||||
|     this.description = data.description | ||||
|     this.options = data.options ?? [] | ||||
|     this.guild = guild | ||||
|   } | ||||
| 
 | ||||
|   async delete(): Promise<void> { | ||||
|     await this.slash.delete(this.id, this._guild) | ||||
|   } | ||||
| 
 | ||||
|   async edit(data: SlashCommandPartial): Promise<void> { | ||||
|     await this.slash.edit(this.id, data, this._guild) | ||||
|   } | ||||
| 
 | ||||
|   /** Create a handler for this Slash Command */ | ||||
|   handle( | ||||
|     func: SlashCommandHandlerCallback, | ||||
|     options?: { parent?: string; group?: string } | ||||
|   ): SlashCommand { | ||||
|     this.slash.slash.handle({ | ||||
|       name: this.name, | ||||
|       parent: options?.parent, | ||||
|       group: options?.group, | ||||
|       guild: this._guild, | ||||
|       handler: func | ||||
|     }) | ||||
|     return this | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export interface CreateOptions { | ||||
|   name: string | ||||
|   description?: string | ||||
|   options?: Array<SlashCommandOption | SlashOptionCallable> | ||||
|   choices?: Array<SlashCommandChoice | string> | ||||
| } | ||||
| 
 | ||||
| function createSlashOption( | ||||
|   type: SlashCommandOptionType, | ||||
|   data: CreateOptions | ||||
| ): SlashCommandOption { | ||||
|   return { | ||||
|     name: data.name, | ||||
|     type, | ||||
|     description: | ||||
|       type === 0 || type === 1 | ||||
|         ? undefined | ||||
|         : data.description ?? 'No description.', | ||||
|     options: data.options?.map((e) => | ||||
|       typeof e === 'function' ? e(SlashOption) : e | ||||
|     ), | ||||
|     choices: | ||||
|       data.choices === undefined | ||||
|         ? undefined | ||||
|         : data.choices.map((e) => | ||||
|             typeof e === 'string' ? { name: e, value: e } : e | ||||
|           ) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 | ||||
| export class SlashOption { | ||||
|   static string(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.STRING, data) | ||||
|   } | ||||
| 
 | ||||
|   static bool(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.BOOLEAN, data) | ||||
|   } | ||||
| 
 | ||||
|   static subCommand(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data) | ||||
|   } | ||||
| 
 | ||||
|   static subCommandGroup(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data) | ||||
|   } | ||||
| 
 | ||||
|   static role(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.ROLE, data) | ||||
|   } | ||||
| 
 | ||||
|   static channel(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.CHANNEL, data) | ||||
|   } | ||||
| 
 | ||||
|   static user(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.USER, data) | ||||
|   } | ||||
| 
 | ||||
|   static number(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.INTEGER, data) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption | ||||
| 
 | ||||
| export type SlashBuilderOptionsData = | ||||
|   | Array<SlashCommandOption | SlashOptionCallable> | ||||
|   | { | ||||
|       [name: string]: | ||||
|         | { | ||||
|             description: string | ||||
|             type: SlashCommandOptionType | ||||
|             options?: SlashCommandOption[] | ||||
|             choices?: SlashCommandChoice[] | ||||
|           } | ||||
|         | SlashOptionCallable | ||||
|     } | ||||
| 
 | ||||
| function buildOptionsArray( | ||||
|   options: SlashBuilderOptionsData | ||||
| ): SlashCommandOption[] { | ||||
|   return Array.isArray(options) | ||||
|     ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op)) | ||||
|     : Object.entries(options).map((entry) => | ||||
|         typeof entry[1] === 'function' | ||||
|           ? entry[1](SlashOption) | ||||
|           : Object.assign(entry[1], { name: entry[0] }) | ||||
|       ) | ||||
| } | ||||
| 
 | ||||
| /** Slash Command Builder */ | ||||
| export class SlashBuilder { | ||||
|   data: SlashCommandPartial | ||||
| 
 | ||||
|   constructor( | ||||
|     name?: string, | ||||
|     description?: string, | ||||
|     options?: SlashBuilderOptionsData | ||||
|   ) { | ||||
|     this.data = { | ||||
|       name: name ?? '', | ||||
|       description: description ?? 'No description.', | ||||
|       options: options === undefined ? [] : buildOptionsArray(options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   name(name: string): SlashBuilder { | ||||
|     this.data.name = name | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   description(desc: string): SlashBuilder { | ||||
|     this.data.description = desc | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { | ||||
|     if (this.data.options === undefined) this.data.options = [] | ||||
|     this.data.options.push( | ||||
|       typeof option === 'function' ? option(SlashOption) : option | ||||
|     ) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   options(options: SlashBuilderOptionsData): SlashBuilder { | ||||
|     this.data.options = buildOptionsArray(options) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   export(): SlashCommandPartial { | ||||
|     if (this.data.name === '') | ||||
|       throw new Error('Name was not provided in Slash Builder') | ||||
|     return this.data | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ | ||||
| export class SlashCommandsManager { | ||||
|   slash: SlashClient | ||||
|   rest: RESTManager | ||||
| 
 | ||||
|   constructor(client: SlashClient) { | ||||
|     this.slash = client | ||||
|     this.rest = client.rest | ||||
|   } | ||||
| 
 | ||||
|   /** Get all Global Slash Commands */ | ||||
|   async all(): Promise<Collection<string, SlashCommand>> { | ||||
|     const col = new Collection<string, SlashCommand>() | ||||
| 
 | ||||
|     const res = (await this.rest.api.applications[ | ||||
|       this.slash.getID() | ||||
|     ].commands.get()) as SlashCommandPayload[] | ||||
|     if (!Array.isArray(res)) return col | ||||
| 
 | ||||
|     for (const raw of res) { | ||||
|       const cmd = new SlashCommand(this, raw) | ||||
|       col.set(raw.id, cmd) | ||||
|     } | ||||
| 
 | ||||
|     return col | ||||
|   } | ||||
| 
 | ||||
|   /** Get a Guild's Slash Commands */ | ||||
|   async guild( | ||||
|     guild: Guild | string | ||||
|   ): Promise<Collection<string, SlashCommand>> { | ||||
|     const col = new Collection<string, SlashCommand>() | ||||
| 
 | ||||
|     const res = (await this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|       typeof guild === 'string' ? guild : guild.id | ||||
|     ].commands.get()) as SlashCommandPayload[] | ||||
|     if (!Array.isArray(res)) return col | ||||
| 
 | ||||
|     const _guild = | ||||
|       typeof guild === 'object' | ||||
|         ? guild | ||||
|         : await this.slash.client?.guilds.get(guild) | ||||
| 
 | ||||
|     for (const raw of res) { | ||||
|       const cmd = new SlashCommand(this, raw, _guild) | ||||
|       cmd._guild = typeof guild === 'string' ? guild : guild.id | ||||
|       col.set(raw.id, cmd) | ||||
|     } | ||||
| 
 | ||||
|     return col | ||||
|   } | ||||
| 
 | ||||
|   /** Create a Slash Command (global or Guild) */ | ||||
|   async create( | ||||
|     data: SlashCommandPartial, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommand> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands | ||||
| 
 | ||||
|     const payload = await route.post(data) | ||||
| 
 | ||||
|     const _guild = | ||||
|       typeof guild === 'object' | ||||
|         ? guild | ||||
|         : guild === undefined | ||||
|         ? undefined | ||||
|         : await this.slash.client?.guilds.get(guild) | ||||
| 
 | ||||
|     const cmd = new SlashCommand(this, payload, _guild) | ||||
|     cmd._guild = | ||||
|       typeof guild === 'string' || guild === undefined ? guild : guild.id | ||||
| 
 | ||||
|     return cmd | ||||
|   } | ||||
| 
 | ||||
|   /** Edit a Slash Command (global or Guild) */ | ||||
|   async edit( | ||||
|     id: string, | ||||
|     data: SlashCommandPartial, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommandsManager> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands[id] | ||||
| 
 | ||||
|     await route.patch(data) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Delete a Slash Command (global or Guild) */ | ||||
|   async delete( | ||||
|     id: string, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommandsManager> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands[id] | ||||
| 
 | ||||
|     await route.delete() | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Get a Slash Command (global or Guild) */ | ||||
|   async get(id: string, guild?: Guild | string): Promise<SlashCommand> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands[id] | ||||
| 
 | ||||
|     const data = await route.get() | ||||
| 
 | ||||
|     const _guild = | ||||
|       typeof guild === 'object' | ||||
|         ? guild | ||||
|         : guild === undefined | ||||
|         ? undefined | ||||
|         : await this.slash.client?.guilds.get(guild) | ||||
| 
 | ||||
|     return new SlashCommand(this, data, _guild) | ||||
|   } | ||||
| 
 | ||||
|   /** Bulk Edit Global or Guild Slash Commands */ | ||||
|   async bulkEdit( | ||||
|     cmds: Array<SlashCommandPartial | SlashCommandPayload>, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommandsManager> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands | ||||
| 
 | ||||
|     await route.put(cmds) | ||||
| 
 | ||||
|     return this | ||||
|   } | ||||
| } | ||||
| import { SlashCommandsManager } from './slashCommand.ts' | ||||
| 
 | ||||
| export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown | ||||
| export interface SlashCommandHandler { | ||||
|  | @ -455,8 +112,38 @@ export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | |||
|   } | ||||
| 
 | ||||
|   /** Adds a new Slash Command Handler */ | ||||
|   handle(handler: SlashCommandHandler): SlashClient { | ||||
|     this.handlers.push(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 | ||||
|   } | ||||
| 
 | ||||
|  | @ -523,7 +210,9 @@ export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | |||
|     ) | ||||
|       return | ||||
| 
 | ||||
|     const cmd = this._getCommand(interaction) | ||||
|     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) | ||||
|  | @ -564,7 +253,7 @@ export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | |||
|     respond: (options: { | ||||
|       status?: number | ||||
|       headers?: Headers | ||||
|       body?: string | Uint8Array | FormData | ||||
|       body?: any | ||||
|     }) => Promise<void> | ||||
|   }): Promise<false | Interaction> { | ||||
|     if (req.method.toLowerCase() !== 'post') return false | ||||
							
								
								
									
										349
									
								
								src/interactions/slashCommand.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								src/interactions/slashCommand.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,349 @@ | |||
| import { RESTManager } from '../rest/manager.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { | ||||
|   SlashCommandChoice, | ||||
|   SlashCommandOption, | ||||
|   SlashCommandOptionType, | ||||
|   SlashCommandPartial, | ||||
|   SlashCommandPayload | ||||
| } from '../types/slash.ts' | ||||
| import { Collection } from '../utils/collection.ts' | ||||
| import type { SlashClient, SlashCommandHandlerCallback } from './slashClient.ts' | ||||
| 
 | ||||
| export class SlashCommand { | ||||
|   slash: SlashCommandsManager | ||||
|   id: string | ||||
|   applicationID: string | ||||
|   name: string | ||||
|   description: string | ||||
|   options: SlashCommandOption[] | ||||
|   guild?: Guild | ||||
|   _guild?: string | ||||
| 
 | ||||
|   constructor( | ||||
|     manager: SlashCommandsManager, | ||||
|     data: SlashCommandPayload, | ||||
|     guild?: Guild | ||||
|   ) { | ||||
|     this.slash = manager | ||||
|     this.id = data.id | ||||
|     this.applicationID = data.application_id | ||||
|     this.name = data.name | ||||
|     this.description = data.description | ||||
|     this.options = data.options ?? [] | ||||
|     this.guild = guild | ||||
|   } | ||||
| 
 | ||||
|   async delete(): Promise<void> { | ||||
|     await this.slash.delete(this.id, this._guild) | ||||
|   } | ||||
| 
 | ||||
|   async edit(data: SlashCommandPartial): Promise<void> { | ||||
|     await this.slash.edit(this.id, data, this._guild) | ||||
|   } | ||||
| 
 | ||||
|   /** Create a handler for this Slash Command */ | ||||
|   handle( | ||||
|     func: SlashCommandHandlerCallback, | ||||
|     options?: { parent?: string; group?: string } | ||||
|   ): SlashCommand { | ||||
|     this.slash.slash.handle({ | ||||
|       name: this.name, | ||||
|       parent: options?.parent, | ||||
|       group: options?.group, | ||||
|       guild: this._guild, | ||||
|       handler: func | ||||
|     }) | ||||
|     return this | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export interface CreateOptions { | ||||
|   name: string | ||||
|   description?: string | ||||
|   options?: Array<SlashCommandOption | SlashOptionCallable> | ||||
|   choices?: Array<SlashCommandChoice | string> | ||||
| } | ||||
| 
 | ||||
| function createSlashOption( | ||||
|   type: SlashCommandOptionType, | ||||
|   data: CreateOptions | ||||
| ): SlashCommandOption { | ||||
|   return { | ||||
|     name: data.name, | ||||
|     type, | ||||
|     description: | ||||
|       type === 0 || type === 1 | ||||
|         ? undefined | ||||
|         : data.description ?? 'No description.', | ||||
|     options: data.options?.map((e) => | ||||
|       typeof e === 'function' ? e(SlashOption) : e | ||||
|     ), | ||||
|     choices: | ||||
|       data.choices === undefined | ||||
|         ? undefined | ||||
|         : data.choices.map((e) => | ||||
|             typeof e === 'string' ? { name: e, value: e } : e | ||||
|           ) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 | ||||
| export class SlashOption { | ||||
|   static string(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.STRING, data) | ||||
|   } | ||||
| 
 | ||||
|   static bool(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.BOOLEAN, data) | ||||
|   } | ||||
| 
 | ||||
|   static subCommand(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.SUB_COMMAND, data) | ||||
|   } | ||||
| 
 | ||||
|   static subCommandGroup(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.SUB_COMMAND_GROUP, data) | ||||
|   } | ||||
| 
 | ||||
|   static role(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.ROLE, data) | ||||
|   } | ||||
| 
 | ||||
|   static channel(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.CHANNEL, data) | ||||
|   } | ||||
| 
 | ||||
|   static user(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.USER, data) | ||||
|   } | ||||
| 
 | ||||
|   static number(data: CreateOptions): SlashCommandOption { | ||||
|     return createSlashOption(SlashCommandOptionType.INTEGER, data) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export type SlashOptionCallable = (o: typeof SlashOption) => SlashCommandOption | ||||
| 
 | ||||
| export type SlashBuilderOptionsData = | ||||
|   | Array<SlashCommandOption | SlashOptionCallable> | ||||
|   | { | ||||
|       [name: string]: | ||||
|         | { | ||||
|             description: string | ||||
|             type: SlashCommandOptionType | ||||
|             options?: SlashCommandOption[] | ||||
|             choices?: SlashCommandChoice[] | ||||
|           } | ||||
|         | SlashOptionCallable | ||||
|     } | ||||
| 
 | ||||
| function buildOptionsArray( | ||||
|   options: SlashBuilderOptionsData | ||||
| ): SlashCommandOption[] { | ||||
|   return Array.isArray(options) | ||||
|     ? options.map((op) => (typeof op === 'function' ? op(SlashOption) : op)) | ||||
|     : Object.entries(options).map((entry) => | ||||
|         typeof entry[1] === 'function' | ||||
|           ? entry[1](SlashOption) | ||||
|           : Object.assign(entry[1], { name: entry[0] }) | ||||
|       ) | ||||
| } | ||||
| 
 | ||||
| /** Slash Command Builder */ | ||||
| export class SlashBuilder { | ||||
|   data: SlashCommandPartial | ||||
| 
 | ||||
|   constructor( | ||||
|     name?: string, | ||||
|     description?: string, | ||||
|     options?: SlashBuilderOptionsData | ||||
|   ) { | ||||
|     this.data = { | ||||
|       name: name ?? '', | ||||
|       description: description ?? 'No description.', | ||||
|       options: options === undefined ? [] : buildOptionsArray(options) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   name(name: string): SlashBuilder { | ||||
|     this.data.name = name | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   description(desc: string): SlashBuilder { | ||||
|     this.data.description = desc | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   option(option: SlashOptionCallable | SlashCommandOption): SlashBuilder { | ||||
|     if (this.data.options === undefined) this.data.options = [] | ||||
|     this.data.options.push( | ||||
|       typeof option === 'function' ? option(SlashOption) : option | ||||
|     ) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   options(options: SlashBuilderOptionsData): SlashBuilder { | ||||
|     this.data.options = buildOptionsArray(options) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   export(): SlashCommandPartial { | ||||
|     if (this.data.name === '') | ||||
|       throw new Error('Name was not provided in Slash Builder') | ||||
|     return this.data | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ | ||||
| export class SlashCommandsManager { | ||||
|   slash: SlashClient | ||||
|   rest: RESTManager | ||||
| 
 | ||||
|   constructor(client: SlashClient) { | ||||
|     this.slash = client | ||||
|     this.rest = client.rest | ||||
|   } | ||||
| 
 | ||||
|   /** Get all Global Slash Commands */ | ||||
|   async all(): Promise<Collection<string, SlashCommand>> { | ||||
|     const col = new Collection<string, SlashCommand>() | ||||
| 
 | ||||
|     const res = (await this.rest.api.applications[ | ||||
|       this.slash.getID() | ||||
|     ].commands.get()) as SlashCommandPayload[] | ||||
|     if (!Array.isArray(res)) return col | ||||
| 
 | ||||
|     for (const raw of res) { | ||||
|       const cmd = new SlashCommand(this, raw) | ||||
|       col.set(raw.id, cmd) | ||||
|     } | ||||
| 
 | ||||
|     return col | ||||
|   } | ||||
| 
 | ||||
|   /** Get a Guild's Slash Commands */ | ||||
|   async guild( | ||||
|     guild: Guild | string | ||||
|   ): Promise<Collection<string, SlashCommand>> { | ||||
|     const col = new Collection<string, SlashCommand>() | ||||
| 
 | ||||
|     const res = (await this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|       typeof guild === 'string' ? guild : guild.id | ||||
|     ].commands.get()) as SlashCommandPayload[] | ||||
|     if (!Array.isArray(res)) return col | ||||
| 
 | ||||
|     const _guild = | ||||
|       typeof guild === 'object' | ||||
|         ? guild | ||||
|         : await this.slash.client?.guilds.get(guild) | ||||
| 
 | ||||
|     for (const raw of res) { | ||||
|       const cmd = new SlashCommand(this, raw, _guild) | ||||
|       cmd._guild = typeof guild === 'string' ? guild : guild.id | ||||
|       col.set(raw.id, cmd) | ||||
|     } | ||||
| 
 | ||||
|     return col | ||||
|   } | ||||
| 
 | ||||
|   /** Create a Slash Command (global or Guild) */ | ||||
|   async create( | ||||
|     data: SlashCommandPartial, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommand> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands | ||||
| 
 | ||||
|     const payload = await route.post(data) | ||||
| 
 | ||||
|     const _guild = | ||||
|       typeof guild === 'object' | ||||
|         ? guild | ||||
|         : guild === undefined | ||||
|         ? undefined | ||||
|         : await this.slash.client?.guilds.get(guild) | ||||
| 
 | ||||
|     const cmd = new SlashCommand(this, payload, _guild) | ||||
|     cmd._guild = | ||||
|       typeof guild === 'string' || guild === undefined ? guild : guild.id | ||||
| 
 | ||||
|     return cmd | ||||
|   } | ||||
| 
 | ||||
|   /** Edit a Slash Command (global or Guild) */ | ||||
|   async edit( | ||||
|     id: string, | ||||
|     data: SlashCommandPartial, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommandsManager> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands[id] | ||||
| 
 | ||||
|     await route.patch(data) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Delete a Slash Command (global or Guild) */ | ||||
|   async delete( | ||||
|     id: string, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommandsManager> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands[id] | ||||
| 
 | ||||
|     await route.delete() | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Get a Slash Command (global or Guild) */ | ||||
|   async get(id: string, guild?: Guild | string): Promise<SlashCommand> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands[id] | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands[id] | ||||
| 
 | ||||
|     const data = await route.get() | ||||
| 
 | ||||
|     const _guild = | ||||
|       typeof guild === 'object' | ||||
|         ? guild | ||||
|         : guild === undefined | ||||
|         ? undefined | ||||
|         : await this.slash.client?.guilds.get(guild) | ||||
| 
 | ||||
|     return new SlashCommand(this, data, _guild) | ||||
|   } | ||||
| 
 | ||||
|   /** Bulk Edit Global or Guild Slash Commands */ | ||||
|   async bulkEdit( | ||||
|     cmds: Array<SlashCommandPartial | SlashCommandPayload>, | ||||
|     guild?: Guild | string | ||||
|   ): Promise<SlashCommandsManager> { | ||||
|     const route = | ||||
|       guild === undefined | ||||
|         ? this.rest.api.applications[this.slash.getID()].commands | ||||
|         : this.rest.api.applications[this.slash.getID()].guilds[ | ||||
|             typeof guild === 'string' ? guild : guild.id | ||||
|           ].commands | ||||
| 
 | ||||
|     await route.put(cmds) | ||||
| 
 | ||||
|     return this | ||||
|   } | ||||
| } | ||||
|  | @ -1,4 +1,4 @@ | |||
| import { SlashCommandHandler } from './slashClient.ts' | ||||
| import type { SlashCommandHandler } from './slashClient.ts' | ||||
| 
 | ||||
| export class SlashModule { | ||||
|   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' | ||||
| 
 | ||||
| /** | ||||
|  | @ -63,7 +63,9 @@ export class BaseManager<T, T2> { | |||
|   async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { | ||||
|     const arr = (await this.array()) ?? [] | ||||
|     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 | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import { Collection } from '../utils/collection.ts' | ||||
| import { BaseManager } from './base.ts' | ||||
| 
 | ||||
|  | @ -43,7 +43,9 @@ export class BaseChildManager<T, T2> { | |||
|   async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> { | ||||
|     const arr = (await this.array()) ?? [] | ||||
|     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 | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,15 +1,15 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { Client } from '../client/mod.ts' | ||||
| import { Channel } from '../structures/channel.ts' | ||||
| import { Embed } from '../structures/embed.ts' | ||||
| import { Message } from '../structures/message.ts' | ||||
| import { TextChannel } from '../structures/textChannel.ts' | ||||
| import { | ||||
| import type { TextChannel } from '../structures/textChannel.ts' | ||||
| import type { | ||||
|   ChannelPayload, | ||||
|   GuildChannelPayload, | ||||
|   MessageOptions | ||||
| } from '../types/channel.ts' | ||||
| import { CHANNEL } from '../types/endpoint.ts' | ||||
| import getChannelByType from '../utils/getChannelByType.ts' | ||||
| import getChannelByType from '../utils/channel.ts' | ||||
| import { BaseManager } from './base.ts' | ||||
| 
 | ||||
| export type AllMessageOptions = MessageOptions | Embed | ||||
|  | @ -121,6 +121,10 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> { | |||
|           : undefined | ||||
|     } | ||||
| 
 | ||||
|     if (payload.content === undefined && payload.embed === undefined) { | ||||
|       payload.content = '' | ||||
|     } | ||||
| 
 | ||||
|     const resp = await this.client.rest.api.channels[channelID].messages.post( | ||||
|       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 { EmojiPayload } from '../types/emoji.ts' | ||||
| import type { EmojiPayload } from '../types/emoji.ts' | ||||
| import { GUILD_EMOJI } from '../types/endpoint.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 | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import type { Client } from '../client/mod.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 { GuildVoiceStatesManager } from './guildVoiceStates.ts' | ||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||
| import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||
| 
 | ||||
| export class GuildChannelVoiceStatesManager extends BaseChildManager< | ||||
|   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 { Guild } from '../structures/guild.ts' | ||||
| import { CategoryChannel } from '../structures/guildCategoryChannel.ts' | ||||
| import type { CategoryChannel } from '../structures/guildCategoryChannel.ts' | ||||
| import { | ||||
|   ChannelTypes, | ||||
|   GuildChannelPayload, | ||||
|   OverwritePayload | ||||
| } 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 { BaseChildManager } from './baseChild.ts' | ||||
| import { ChannelsManager } from './channels.ts' | ||||
| import type { ChannelsManager } from './channels.ts' | ||||
| 
 | ||||
| export interface CreateChannelOptions { | ||||
|   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 { Guild } from '../structures/guild.ts' | ||||
| import type { Guild } from '../structures/guild.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 { BaseChildManager } from './baseChild.ts' | ||||
| import { EmojisManager } from './emojis.ts' | ||||
| import type { EmojisManager } from './emojis.ts' | ||||
| import { fetchAuto } from '../../deps.ts' | ||||
| 
 | ||||
| export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||
| import { User } from '../structures/user.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||
| import type { User } from '../structures/user.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' | ||||
| 
 | ||||
| export class GuildVoiceStatesManager extends BaseManager< | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| 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 { Template } from '../structures/template.ts' | ||||
| import type { Template } from '../structures/template.ts' | ||||
| import { Role } from '../structures/role.ts' | ||||
| import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts' | ||||
| import { | ||||
| import type { | ||||
|   GuildPayload, | ||||
|   MemberPayload, | ||||
|   GuildCreateRolePayload, | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { GuildTextChannel, User } from '../../mod.ts' | ||||
| import { Client } from '../models/client.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import type { GuildTextChannel, User } from '../../mod.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { Invite } from '../structures/invite.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' | ||||
| 
 | ||||
| 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 { RolePayload } from '../types/role.ts' | ||||
| import type { RolePayload } from '../types/role.ts' | ||||
| import { Role } from '../structures/role.ts' | ||||
| import { Member } from '../structures/member.ts' | ||||
| import { RolesManager } from './roles.ts' | ||||
| import { MemberPayload } from '../types/guild.ts' | ||||
| import type { Member } from '../structures/member.ts' | ||||
| import type { RolesManager } from './roles.ts' | ||||
| import type { MemberPayload } from '../types/guild.ts' | ||||
| import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts' | ||||
| 
 | ||||
| export class MemberRolesManager extends BaseChildManager<RolePayload, Role> { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { User } from '../structures/user.ts' | ||||
| import { Client } from '../models/client.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { Member } from '../structures/member.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 { 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 { Guild } from '../structures/guild.ts' | ||||
| import { Message } from '../structures/message.ts' | ||||
| import type { Message } from '../structures/message.ts' | ||||
| import { MessageReaction } from '../structures/messageReaction.ts' | ||||
| import { User } from '../structures/user.ts' | ||||
| import { Reaction } from '../types/channel.ts' | ||||
| import type { User } from '../structures/user.ts' | ||||
| import type { Reaction } from '../types/channel.ts' | ||||
| import { | ||||
|   MESSAGE_REACTION, | ||||
|   MESSAGE_REACTIONS, | ||||
|  | @ -19,7 +18,7 @@ export class MessageReactionsManager extends BaseManager< | |||
|   message: Message | ||||
| 
 | ||||
|   constructor(client: Client, message: Message) { | ||||
|     super(client, `reactions:${message.id}`, Guild) | ||||
|     super(client, `reactions:${message.id}`, MessageReaction) | ||||
|     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 { TextChannel } from '../structures/textChannel.ts' | ||||
| import type { TextChannel } from '../structures/textChannel.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 { BaseManager } from './base.ts' | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { Presence } from '../structures/presence.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' | ||||
| 
 | ||||
| export class GuildPresencesManager extends BaseManager< | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { MessageReaction } from '../structures/messageReaction.ts' | ||||
| import { User } from '../structures/user.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { MessageReaction } from '../structures/messageReaction.ts' | ||||
| import type { User } from '../structures/user.ts' | ||||
| import { UsersManager } from './users.ts' | ||||
| 
 | ||||
| export class ReactionUsersManager extends UsersManager { | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| import { Permissions } from '../../mod.ts' | ||||
| import { Client } from '../models/client.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { Guild } from '../structures/guild.ts' | ||||
| import { Role } from '../structures/role.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' | ||||
| 
 | ||||
| export interface CreateGuildRoleOptions { | ||||
|  |  | |||
|  | @ -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 '../types/endpoint.ts' | ||||
| import { UserPayload } from '../types/user.ts' | ||||
| import type { UserPayload } from '../types/user.ts' | ||||
| import { BaseManager } from './base.ts' | ||||
| 
 | ||||
| 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 { ApplicationPayload } from '../types/application.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { ApplicationPayload } from '../types/application.ts' | ||||
| import { SnowflakeBase } from './base.ts' | ||||
| import { User } from './user.ts' | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import { Snowflake } from '../utils/snowflake.ts' | ||||
| 
 | ||||
| export class Base { | ||||
|  |  | |||
|  | @ -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 */ | ||||
| export const ImageURL = ( | ||||
|  |  | |||
|  | @ -1,20 +1,20 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { | ||||
|   ChannelPayload, | ||||
|   ChannelTypes, | ||||
|   ModifyChannelOption, | ||||
|   ModifyChannelPayload, | ||||
|   Overwrite, | ||||
|   OverwritePayload, | ||||
|   OverwriteAsArg, | ||||
|   OverrideType | ||||
|   OverwriteAsArg | ||||
| } from '../types/channel.ts' | ||||
| import { OverrideType } from '../types/channel.ts' | ||||
| import { CHANNEL } from '../types/endpoint.ts' | ||||
| import { GuildChannelPayloads, GuildChannels } from '../types/guild.ts' | ||||
| import getChannelByType from '../utils/getChannelByType.ts' | ||||
| import type { GuildChannelPayloads, GuildChannels } from '../types/guild.ts' | ||||
| import getChannelByType from '../utils/channel.ts' | ||||
| import { Permissions } from '../utils/permissions.ts' | ||||
| import { SnowflakeBase } from './base.ts' | ||||
| import { Guild } from './guild.ts' | ||||
| import type { Guild } from './guild.ts' | ||||
| import { Member } from './member.ts' | ||||
| import { Role } from './role.ts' | ||||
| 
 | ||||
|  | @ -81,7 +81,7 @@ export class GuildChannel extends Channel { | |||
|     const stringToObject = | ||||
|       typeof target === 'string' | ||||
|         ? (await this.guild.members.get(target)) ?? | ||||
|         (await this.guild.roles.get(target)) | ||||
|           (await this.guild.roles.get(target)) | ||||
|         : target | ||||
| 
 | ||||
|     if (stringToObject === undefined) { | ||||
|  | @ -128,7 +128,7 @@ export class GuildChannel extends Channel { | |||
|     const stringToObject = | ||||
|       typeof target === 'string' | ||||
|         ? (await this.guild.members.get(target)) ?? | ||||
|         (await this.guild.roles.get(target)) | ||||
|           (await this.guild.roles.get(target)) | ||||
|         : target | ||||
| 
 | ||||
|     if (stringToObject === undefined) { | ||||
|  | @ -200,8 +200,8 @@ export class GuildChannel extends Channel { | |||
|           overwrite.id instanceof Role | ||||
|             ? 0 | ||||
|             : overwrite.id instanceof Member | ||||
|               ? 1 | ||||
|               : overwrite.type | ||||
|             ? 1 | ||||
|             : overwrite.type | ||||
|         if (type === undefined) { | ||||
|           throw new Error('Overwrite type is undefined.') | ||||
|         } | ||||
|  | @ -233,8 +233,8 @@ export class GuildChannel extends Channel { | |||
|       overwrite.id instanceof Role | ||||
|         ? 0 | ||||
|         : overwrite.id instanceof Member | ||||
|           ? 1 | ||||
|           : overwrite.type | ||||
|         ? 1 | ||||
|         : overwrite.type | ||||
|     if (type === undefined) { | ||||
|       throw new Error('Overwrite type is undefined.') | ||||
|     } | ||||
|  | @ -303,7 +303,10 @@ export class GuildChannel extends Channel { | |||
|           : overwrite.allow?.toJSON() ?? overwrites[index].allow | ||||
|     } | ||||
| 
 | ||||
|     if (overwrite.deny !== undefined && overwriteDeny !== OverrideType.REPLACE) { | ||||
|     if ( | ||||
|       overwrite.deny !== undefined && | ||||
|       overwriteDeny !== OverrideType.REPLACE | ||||
|     ) { | ||||
|       switch (overwriteDeny) { | ||||
|         case OverrideType.ADD: { | ||||
|           const originalDeny = new Permissions(overwrites[index].deny) | ||||
|  | @ -331,8 +334,8 @@ export class GuildChannel extends Channel { | |||
|       overwrite.id instanceof Role | ||||
|         ? 0 | ||||
|         : overwrite.id instanceof Member | ||||
|           ? 1 | ||||
|           : overwrite.type | ||||
|         ? 1 | ||||
|         : overwrite.type | ||||
|     if (type === undefined) { | ||||
|       throw new Error('Overwrite type is undefined.') | ||||
|     } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { DMChannelPayload } from '../types/channel.ts' | ||||
| import { UserPayload } from '../types/user.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { DMChannelPayload } from '../types/channel.ts' | ||||
| import type { UserPayload } from '../types/user.ts' | ||||
| import { TextChannel } from './textChannel.ts' | ||||
| 
 | ||||
| export class DMChannel extends TextChannel { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { | ||||
| import type { | ||||
|   EmbedAuthor, | ||||
|   EmbedField, | ||||
|   EmbedFooter, | ||||
|  | @ -10,7 +10,7 @@ import { | |||
|   EmbedVideo | ||||
| } from '../types/channel.ts' | ||||
| import { Colors, ColorUtil } from '../utils/colorutil.ts' | ||||
| import { MessageAttachment } from './message.ts' | ||||
| import type { MessageAttachment } from './message.ts' | ||||
| 
 | ||||
| /** Message Embed Object */ | ||||
| export class Embed { | ||||
|  | @ -56,44 +56,72 @@ export class Embed { | |||
| 
 | ||||
|   /** Convert Embed Object to Embed Payload JSON */ | ||||
|   toJSON(): EmbedPayload { | ||||
|     let total = 0; | ||||
|     if (this.title?.length !== undefined && this.title?.length > Embed.MAX_TITLE_LENGTH) { | ||||
|     let total = 0 | ||||
|     if ( | ||||
|       this.title?.length !== undefined && | ||||
|       this.title?.length > Embed.MAX_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) | ||||
|       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) { | ||||
|       this.fields.forEach((field) => { | ||||
|         if (field.name.length > Embed.MAX_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) { | ||||
|           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) | ||||
|       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) | ||||
|       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 { | ||||
|       title: this.title, | ||||
|       type: this.type, | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { ImageSize } from '../types/cdn.ts' | ||||
| import { EmojiPayload } from '../types/emoji.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { ImageSize } from '../types/cdn.ts' | ||||
| import type { EmojiPayload } from '../types/emoji.ts' | ||||
| import { CUSTOM_EMOJI, EMOJI } from '../types/endpoint.ts' | ||||
| import { Snowflake } from '../utils/snowflake.ts' | ||||
| import { Base } from './base.ts' | ||||
| import { ImageURL } from './cdn.ts' | ||||
| import { Guild } from './guild.ts' | ||||
| import type { Guild } from './guild.ts' | ||||
| import { Role } from './role.ts' | ||||
| import { User } from './user.ts' | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { GroupDMChannelPayload } from '../types/channel.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import type { GroupDMChannelPayload } from '../types/channel.ts' | ||||
| import { Channel } from './channel.ts' | ||||
| 
 | ||||
| export class GroupDMChannel extends Channel { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import type { Client } from '../client/mod.ts' | ||||
| import { | ||||
|   GuildBanPayload, | ||||
|   GuildFeatures, | ||||
|  | @ -41,12 +41,12 @@ import { | |||
|   GUILD_SPLASH | ||||
| } from '../types/endpoint.ts' | ||||
| import { GuildVoiceStatesManager } from '../managers/guildVoiceStates.ts' | ||||
| import { RequestMembersOptions } from '../gateway/index.ts' | ||||
| import type { RequestMembersOptions } from '../gateway/mod.ts' | ||||
| import { GuildPresencesManager } from '../managers/presences.ts' | ||||
| import { TemplatePayload } from '../types/template.ts' | ||||
| import type { TemplatePayload } from '../types/template.ts' | ||||
| import { Template } from './template.ts' | ||||
| import { DiscordAPIError } from '../models/rest.ts' | ||||
| import { ImageFormats, ImageSize } from '../types/cdn.ts' | ||||
| import { DiscordAPIError } from '../rest/mod.ts' | ||||
| import type { ImageFormats, ImageSize } from '../types/cdn.ts' | ||||
| import { ImageURL } from './cdn.ts' | ||||
| 
 | ||||
| export class GuildBan extends Base { | ||||
|  |  | |||
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