feat: port to typed EventEmitter
This commit is contained in:
		
							parent
							
								
									3f436b2b3f
								
							
						
					
					
						commit
						e7b0804616
					
				
					 13 changed files with 318 additions and 198 deletions
				
			
		
							
								
								
									
										2
									
								
								deps.ts
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								deps.ts
									
										
									
									
									
								
							|  | @ -1,4 +1,4 @@ | |||
| export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts' | ||||
| export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' | ||||
| export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts' | ||||
| export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts' | ||||
| export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts' | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| import { GatewayEventHandler } from '../index.ts' | ||||
| import { GatewayEvents, TypingStartGuildData } from '../../types/gateway.ts' | ||||
| import { | ||||
|   GatewayEvents, | ||||
|   MessageDeletePayload, | ||||
|   TypingStartGuildData | ||||
| } from '../../types/gateway.ts' | ||||
| import { channelCreate } from './channelCreate.ts' | ||||
| import { channelDelete } from './channelDelete.ts' | ||||
| import { channelUpdate } from './channelUpdate.ts' | ||||
|  | @ -55,6 +59,10 @@ import { | |||
| } from '../../utils/getChannelByType.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 { GuildChannel } from '../../managers/guildChannels.ts' | ||||
| 
 | ||||
| export const gatewayHandlers: { | ||||
|   [eventCode in GatewayEvents]: GatewayEventHandler | undefined | ||||
|  | @ -105,7 +113,8 @@ export interface VoiceServerUpdateData { | |||
|   guild: Guild | ||||
| } | ||||
| 
 | ||||
| export interface ClientEvents { | ||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||
| export type ClientEvents = { | ||||
|   /** When Client has successfully connected to Discord */ | ||||
|   ready: [] | ||||
|   /** When a successful reconnect has been made */ | ||||
|  | @ -355,4 +364,40 @@ export interface ClientEvents { | |||
|    * @param payload Payload JSON of the event | ||||
|    */ | ||||
|   raw: [evt: string, payload: any] | ||||
| 
 | ||||
|   /** | ||||
|    * An uncached Message was deleted. | ||||
|    * @param payload Message Delete Payload | ||||
|    */ | ||||
|   messageDeleteUncached: [payload: MessageDeletePayload] | ||||
| 
 | ||||
|   guildMembersChunk: [ | ||||
|     guild: Guild, | ||||
|     info: { | ||||
|       chunkIndex: number | ||||
|       chunkCount: number | ||||
|       members: string[] | ||||
|       presences: string[] | undefined | ||||
|     } | ||||
|   ] | ||||
|   guildMembersChunked: [guild: Guild, chunks: number] | ||||
|   rateLimit: [data: { method: RequestMethods; url: string; body: any }] | ||||
|   inviteDeleteUncached: [invite: PartialInvitePayload] | ||||
|   voiceStateRemoveUncached: [data: { guild: Guild; member: Member }] | ||||
|   userUpdateUncached: [user: User] | ||||
|   webhooksUpdateUncached: [guild: Guild, channelID: string] | ||||
|   guildRoleUpdateUncached: [role: Role] | ||||
|   guildMemberUpdateUncached: [member: Member] | ||||
|   guildMemberRemoveUncached: [member: Member] | ||||
|   channelUpdateUncached: [channel: GuildChannel] | ||||
| 
 | ||||
|   commandOwnerOnly: [ctx: CommandContext] | ||||
|   commandGuildOnly: [ctx: CommandContext] | ||||
|   commandDmOnly: [ctx: CommandContext] | ||||
|   commandNSFW: [ctx: CommandContext] | ||||
|   commandBotMissingPermissions: [ctx: CommandContext, missing: string[]] | ||||
|   commandUserMissingPermissions: [ctx: CommandContext, missing: string[]] | ||||
|   commandMissingArgs: [ctx: CommandContext] | ||||
|   commandUsed: [ctx: CommandContext] | ||||
|   commandError: [ctx: CommandContext, err: Error] | ||||
| } | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ export const ready: GatewayEventHandler = async ( | |||
|   gateway: Gateway, | ||||
|   d: Ready | ||||
| ) => { | ||||
|   gateway.client.upSince = new Date() | ||||
|   await gateway.client.guilds.flush() | ||||
| 
 | ||||
|   await gateway.client.users.set(d.user.id, d.user) | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ export const resume: GatewayEventHandler = async ( | |||
|   d: Resume | ||||
| ) => { | ||||
|   gateway.debug(`Session Resumed!`) | ||||
|   gateway.client.emit('resume') | ||||
|   gateway.client.emit('resumed') | ||||
|   if (gateway.client.user === undefined) | ||||
|     gateway.client.user = new User( | ||||
|       gateway.client, | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { unzlib, EventEmitter } from '../../deps.ts' | ||||
| import { unzlib } from '../../deps.ts' | ||||
| import { Client } from '../models/client.ts' | ||||
| import { | ||||
|   DISCORD_GATEWAY_URL, | ||||
|  | @ -10,14 +10,15 @@ import { | |||
|   GatewayIntents, | ||||
|   GatewayCloseCodes, | ||||
|   IdentityPayload, | ||||
|   StatusUpdatePayload | ||||
|   StatusUpdatePayload, | ||||
|   GatewayEvents | ||||
| } from '../types/gateway.ts' | ||||
| import { gatewayHandlers } from './handlers/index.ts' | ||||
| import { GATEWAY_BOT } from '../types/endpoint.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 { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| 
 | ||||
| export interface RequestMembersOptions { | ||||
|   limit?: number | ||||
|  | @ -33,15 +34,31 @@ export interface VoiceStateOptions { | |||
| 
 | ||||
| export const RECONNECT_REASON = 'harmony-reconnect' | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||
| export type GatewayTypedEvents = { | ||||
|   [name in GatewayEvents]: [any] | ||||
| } & { | ||||
|   connect: [] | ||||
|   ping: [number] | ||||
|   resume: [] | ||||
|   reconnectRequired: [] | ||||
|   close: [number, string] | ||||
|   error: [Error, ErrorEvent] | ||||
|   sentIdentify: [] | ||||
|   sentResume: [] | ||||
|   reconnecting: [] | ||||
|   init: [] | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Handles Discord Gateway connection. | ||||
|  * | ||||
|  * You should not use this and rather use Client class. | ||||
|  */ | ||||
| export class Gateway extends EventEmitter { | ||||
|   websocket: WebSocket | ||||
|   token: string | ||||
|   intents: GatewayIntents[] | ||||
| export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||
|   websocket?: WebSocket | ||||
|   token?: string | ||||
|   intents?: GatewayIntents[] | ||||
|   connected = false | ||||
|   initialized = false | ||||
|   heartbeatInterval = 0 | ||||
|  | @ -53,23 +70,13 @@ export class Gateway extends EventEmitter { | |||
|   client: Client | ||||
|   cache: GatewayCache | ||||
|   private timedIdentify: number | null = null | ||||
|   shards?: number[] | ||||
| 
 | ||||
|   constructor(client: Client, token: string, intents: GatewayIntents[]) { | ||||
|   constructor(client: Client, shards?: number[]) { | ||||
|     super() | ||||
|     this.token = token | ||||
|     this.intents = intents | ||||
|     this.client = client | ||||
|     this.cache = new GatewayCache(client) | ||||
|     this.websocket = new WebSocket( | ||||
|       // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
 | ||||
|       `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, | ||||
|       [] | ||||
|     ) | ||||
|     this.websocket.binaryType = 'arraybuffer' | ||||
|     this.websocket.onopen = this.onopen.bind(this) | ||||
|     this.websocket.onmessage = this.onmessage.bind(this) | ||||
|     this.websocket.onclose = this.onclose.bind(this) | ||||
|     this.websocket.onerror = this.onerror.bind(this) | ||||
|     this.shards = shards | ||||
|   } | ||||
| 
 | ||||
|   private onopen(): void { | ||||
|  | @ -145,7 +152,7 @@ export class Gateway extends EventEmitter { | |||
|           await this.cache.set('seq', s) | ||||
|         } | ||||
|         if (t !== null && t !== undefined) { | ||||
|           this.emit(t, d) | ||||
|           this.emit(t as any, d) | ||||
|           this.client.emit('raw', t, d) | ||||
| 
 | ||||
|           const handler = gatewayHandlers[t] | ||||
|  | @ -236,24 +243,40 @@ export class Gateway extends EventEmitter { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private onerror(event: Event | ErrorEvent): void { | ||||
|     const eventError = event as ErrorEvent | ||||
|     this.emit('error', eventError) | ||||
|   private async onerror(event: ErrorEvent): Promise<void> { | ||||
|     const error = new Error( | ||||
|       Deno.inspect({ | ||||
|         message: event.message, | ||||
|         error: event.error, | ||||
|         type: event.type, | ||||
|         target: event.target | ||||
|       }) | ||||
|     ) | ||||
|     error.name = 'ErrorEvent' | ||||
|     console.log(error) | ||||
|     this.emit('error', error, event) | ||||
|     await this.reconnect() | ||||
|   } | ||||
| 
 | ||||
|   private async sendIdentify(forceNewSession?: boolean): Promise<void> { | ||||
|     this.debug('Fetching /gateway/bot...') | ||||
|     const info = await this.client.rest.get(GATEWAY_BOT()) | ||||
|     if (info.session_start_limit.remaining === 0) | ||||
|       throw new Error( | ||||
|         `Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms` | ||||
|     if (typeof this.token !== 'string') throw new Error('Token not specified') | ||||
|     if (typeof this.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(`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`) | ||||
|       this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) | ||||
|     } | ||||
| 
 | ||||
|     if (forceNewSession === undefined || !forceNewSession) { | ||||
|       const sessionIDCached = await this.cache.get('session_id') | ||||
|  | @ -272,7 +295,10 @@ export class Gateway extends EventEmitter { | |||
|         $device: this.client.clientProperties.device ?? 'harmony' | ||||
|       }, | ||||
|       compress: true, | ||||
|       shard: [0, 1], // TODO: Make sharding possible
 | ||||
|       shard: | ||||
|         this.shards === undefined | ||||
|           ? [0, 1] | ||||
|           : [this.shards[0] ?? 0, this.shards[1] ?? 1], | ||||
|       intents: this.intents.reduce( | ||||
|         (previous, current) => previous | current, | ||||
|         0 | ||||
|  | @ -289,6 +315,10 @@ export class Gateway extends EventEmitter { | |||
|   } | ||||
| 
 | ||||
|   private async sendResume(): Promise<void> { | ||||
|     if (typeof this.token !== 'string') throw new Error('Token not specified') | ||||
|     if (typeof this.intents !== 'object') | ||||
|       throw new Error('Intents not specified') | ||||
| 
 | ||||
|     if (this.sessionID === undefined) { | ||||
|       this.sessionID = await this.cache.get('session_id') | ||||
|       if (this.sessionID === undefined) return await this.sendIdentify() | ||||
|  | @ -380,22 +410,22 @@ export class Gateway extends EventEmitter { | |||
|     this.websocket.onopen = this.onopen.bind(this) | ||||
|     this.websocket.onmessage = this.onmessage.bind(this) | ||||
|     this.websocket.onclose = this.onclose.bind(this) | ||||
|     this.websocket.onerror = this.onerror.bind(this) | ||||
|     this.websocket.onerror = this.onerror.bind(this) as any | ||||
|   } | ||||
| 
 | ||||
|   close(code: number = 1000, reason?: string): void { | ||||
|     return this.websocket.close(code, reason) | ||||
|     return this.websocket?.close(code, reason) | ||||
|   } | ||||
| 
 | ||||
|   send(data: GatewayResponse): boolean { | ||||
|     if (this.websocket.readyState !== this.websocket.OPEN) return false | ||||
|     if (this.websocket?.readyState !== this.websocket?.OPEN) return false | ||||
|     const packet = JSON.stringify({ | ||||
|       op: data.op, | ||||
|       d: data.d, | ||||
|       s: typeof data.s === 'number' ? data.s : null, | ||||
|       t: data.t === undefined ? null : data.t | ||||
|     }) | ||||
|     this.websocket.send(packet) | ||||
|     this.websocket?.send(packet) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ 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 { EventEmitter } from '../../deps.ts' | ||||
| import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' | ||||
| import { UsersManager } from '../managers/users.ts' | ||||
| import { GuildManager } from '../managers/guilds.ts' | ||||
|  | @ -21,6 +20,7 @@ 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' | ||||
| 
 | ||||
| /** OS related properties sent with Gateway Identify */ | ||||
| export interface ClientProperties { | ||||
|  | @ -59,40 +59,20 @@ export interface ClientOptions { | |||
|   disableEnvToken?: boolean | ||||
|   /** Override REST Options */ | ||||
|   restOptions?: RESTOptions | ||||
| } | ||||
| 
 | ||||
| export declare interface Client { | ||||
|   on<K extends keyof ClientEvents>( | ||||
|     event: K, | ||||
|     listener: (...args: ClientEvents[K]) => void | ||||
|   ): this | ||||
|   on(event: string | symbol, listener: (...args: any[]) => void): this | ||||
| 
 | ||||
|   once<K extends keyof ClientEvents>( | ||||
|     event: K, | ||||
|     listener: (...args: ClientEvents[K]) => void | ||||
|   ): this | ||||
|   once(event: string | symbol, listener: (...args: any[]) => void): this | ||||
| 
 | ||||
|   emit<K extends keyof ClientEvents>( | ||||
|     event: K, | ||||
|     ...args: ClientEvents[K] | ||||
|   ): boolean | ||||
|   emit(event: string | symbol, ...args: any[]): boolean | ||||
| 
 | ||||
|   off<K extends keyof ClientEvents>( | ||||
|     event: K, | ||||
|     listener: (...args: ClientEvents[K]) => void | ||||
|   ): this | ||||
|   off(event: string | symbol, listener: (...args: any[]) => void): this | ||||
|   /** Whether to fetch Gateway info or not */ | ||||
|   fetchGatewayInfo?: boolean | ||||
|   /** ADVANCED: Shard ID to launch on */ | ||||
|   shard?: number | ||||
|   /** Shard count. Set to 'auto' for automatic sharding */ | ||||
|   shardCount?: number | 'auto' | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Discord Client. | ||||
|  */ | ||||
| export class Client extends EventEmitter { | ||||
| export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||
|   /** Gateway object */ | ||||
|   gateway?: Gateway | ||||
|   gateway: Gateway | ||||
|   /** REST Manager - used to make all requests */ | ||||
|   rest: RESTManager | ||||
|   /** User which Client logs in to, undefined until logs in */ | ||||
|  | @ -117,12 +97,21 @@ export class Client extends EventEmitter { | |||
|   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?: { | ||||
|  | @ -141,10 +130,23 @@ export class Client extends EventEmitter { | |||
| 
 | ||||
|   /** Shard on which this Client is */ | ||||
|   shard: number = 0 | ||||
|   /** Shard Count */ | ||||
|   shardCount: number | 'auto' = 1 | ||||
|   /** Shard Manager of this Client if Sharded */ | ||||
|   shardManager?: ShardManager | ||||
|   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 dif | ||||
|       else return dif | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   constructor(options: ClientOptions = {}) { | ||||
|     super() | ||||
|     this._id = options.id | ||||
|  | @ -169,7 +171,7 @@ export class Client extends EventEmitter { | |||
|       Object.keys(this._decoratedEvents).length !== 0 | ||||
|     ) { | ||||
|       Object.entries(this._decoratedEvents).forEach((entry) => { | ||||
|         this.on(entry[0], entry[1]) | ||||
|         this.on(entry[0] as keyof ClientEvents, entry[1]) | ||||
|       }) | ||||
|       this._decoratedEvents = undefined | ||||
|     } | ||||
|  | @ -183,12 +185,17 @@ export class Client extends EventEmitter { | |||
|           } | ||||
|         : options.clientProperties | ||||
| 
 | ||||
|     if (options.shard !== undefined) this.shard = options.shard | ||||
|     if (options.shardCount !== undefined) this.shardCount = options.shardCount | ||||
| 
 | ||||
|     this.slash = new SlashClient({ | ||||
|       id: () => this.getEstimatedID(), | ||||
|       client: this, | ||||
|       enabled: options.enableSlash | ||||
|     }) | ||||
| 
 | ||||
|     if (options.fetchGatewayInfo === true) this.fetchGatewayInfo = true | ||||
| 
 | ||||
|     if (this.token === undefined) { | ||||
|       try { | ||||
|         const token = Deno.env.get('DISCORD_TOKEN') | ||||
|  | @ -209,6 +216,7 @@ export class Client extends EventEmitter { | |||
|     if (options.restOptions !== undefined) | ||||
|       Object.assign(restOptions, options.restOptions) | ||||
|     this.rest = new RESTManager(restOptions) | ||||
|     this.gateway = new Gateway(this) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -232,6 +240,7 @@ export class Client extends EventEmitter { | |||
| 
 | ||||
|   /** Emits debug event */ | ||||
|   debug(tag: string, msg: string): void { | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|     this.emit('debug', `[${tag}] ${msg}`) | ||||
|   } | ||||
| 
 | ||||
|  | @ -271,7 +280,7 @@ export class Client extends EventEmitter { | |||
|    * @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. | ||||
|    */ | ||||
|   connect(token?: string, intents?: GatewayIntents[]): void { | ||||
|   async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> { | ||||
|     if (token === undefined && this.token !== undefined) token = this.token | ||||
|     else if (this.token === undefined && token !== undefined) { | ||||
|       this.token = token | ||||
|  | @ -288,7 +297,30 @@ export class Client extends EventEmitter { | |||
|     } else throw new Error('No Gateway Intents were provided') | ||||
| 
 | ||||
|     this.rest.token = token | ||||
|     this.gateway = new Gateway(this, token, intents) | ||||
|     this.gateway.token = token | ||||
|     this.gateway.intents = intents | ||||
|     this.gateway.initWebsocket() | ||||
|     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 */ | ||||
|  | @ -309,7 +341,7 @@ export class Client extends EventEmitter { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   emit(event: keyof ClientEvents, ...args: any[]): boolean { | ||||
|   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) | ||||
|  | @ -317,33 +349,11 @@ export class Client extends EventEmitter { | |||
|     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) | ||||
|   } | ||||
| 
 | ||||
|   /** Wait for an Event (optionally satisfying an event) to occur */ | ||||
|   async waitFor<K extends keyof ClientEvents>( | ||||
|     event: K, | ||||
|     checkFunction: (...args: ClientEvents[K]) => boolean, | ||||
|     timeout?: number | ||||
|   ): Promise<ClientEvents[K] | []> { | ||||
|     return await new Promise((resolve) => { | ||||
|       let timeoutID: number | undefined | ||||
|       if (timeout !== undefined) { | ||||
|         timeoutID = setTimeout(() => { | ||||
|           this.off(event, eventFunc) | ||||
|           resolve([]) | ||||
|         }, timeout) | ||||
|       } | ||||
|       const eventFunc = (...args: ClientEvents[K]): void => { | ||||
|         if (checkFunction(...args)) { | ||||
|           resolve(args) | ||||
|           this.off(event, eventFunc) | ||||
|           if (timeoutID !== undefined) clearTimeout(timeoutID) | ||||
|         } | ||||
|       } | ||||
|       this.on(event, eventFunc) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** Event decorator to create an Event handler from function */ | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Collection } from '../utils/collection.ts' | ||||
| import { EventEmitter } from '../../deps.ts' | ||||
| import type { Client } from './client.ts' | ||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| 
 | ||||
| export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean> | ||||
| 
 | ||||
|  | @ -19,7 +19,14 @@ export interface CollectorOptions { | |||
|   timeout?: number | ||||
| } | ||||
| 
 | ||||
| export class Collector extends EventEmitter { | ||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||
| export type CollectorEvents = { | ||||
|   start: [] | ||||
|   end: [] | ||||
|   collect: any | ||||
| } | ||||
| 
 | ||||
| export class Collector extends HarmonyEventEmitter<CollectorEvents> { | ||||
|   client?: Client | ||||
|   private _started: boolean = false | ||||
|   event: string | ||||
|  | @ -146,14 +153,14 @@ export class Collector extends EventEmitter { | |||
|       let done = false | ||||
|       const onend = (): void => { | ||||
|         done = true | ||||
|         this.removeListener('end', onend) | ||||
|         this.off('end', onend) | ||||
|         resolve(this) | ||||
|       } | ||||
| 
 | ||||
|       this.on('end', onend) | ||||
|       setTimeout(() => { | ||||
|         if (!done) { | ||||
|           this.removeListener('end', onend) | ||||
|           this.off('end', onend) | ||||
|           reject(new Error('Timeout')) | ||||
|         } | ||||
|       }, timeout) | ||||
|  |  | |||
|  | @ -259,7 +259,7 @@ export class CommandClient extends Client implements CommandClientOptions { | |||
|         : category.ownerOnly) === true && | ||||
|       !this.owners.includes(msg.author.id) | ||||
|     ) | ||||
|       return this.emit('commandOwnerOnly', ctx, command) | ||||
|       return this.emit('commandOwnerOnly', ctx) | ||||
| 
 | ||||
|     // Checks if Command is only for Guild
 | ||||
|     if ( | ||||
|  | @ -268,7 +268,7 @@ export class CommandClient extends Client implements CommandClientOptions { | |||
|         : category.guildOnly) === true && | ||||
|       msg.guild === undefined | ||||
|     ) | ||||
|       return this.emit('commandGuildOnly', ctx, command) | ||||
|       return this.emit('commandGuildOnly', ctx) | ||||
| 
 | ||||
|     // Checks if Command is only for DMs
 | ||||
|     if ( | ||||
|  | @ -277,14 +277,14 @@ export class CommandClient extends Client implements CommandClientOptions { | |||
|         : category.dmOnly) === true && | ||||
|       msg.guild !== undefined | ||||
|     ) | ||||
|       return this.emit('commandDmOnly', ctx, command) | ||||
|       return this.emit('commandDmOnly', ctx) | ||||
| 
 | ||||
|     if ( | ||||
|       command.nsfw === true && | ||||
|       (msg.guild === undefined || | ||||
|         ((msg.channel as unknown) as GuildTextChannel).nsfw !== true) | ||||
|     ) | ||||
|       return this.emit('commandNSFW', ctx, command) | ||||
|       return this.emit('commandNSFW', ctx) | ||||
| 
 | ||||
|     const allPermissions = | ||||
|       command.permissions !== undefined | ||||
|  | @ -316,12 +316,7 @@ export class CommandClient extends Client implements CommandClientOptions { | |||
|         } | ||||
| 
 | ||||
|         if (missing.length !== 0) | ||||
|           return this.emit( | ||||
|             'commandBotMissingPermissions', | ||||
|             ctx, | ||||
|             command, | ||||
|             missing | ||||
|           ) | ||||
|           return this.emit('commandBotMissingPermissions', ctx, missing) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -349,27 +344,22 @@ export class CommandClient extends Client implements CommandClientOptions { | |||
|         } | ||||
| 
 | ||||
|         if (missing.length !== 0) | ||||
|           return this.emit( | ||||
|             'commandUserMissingPermissions', | ||||
|             command, | ||||
|             missing, | ||||
|             ctx | ||||
|           ) | ||||
|           return this.emit('commandUserMissingPermissions', ctx, missing) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (command.args !== undefined) { | ||||
|       if (typeof command.args === 'boolean' && parsed.args.length === 0) | ||||
|         return this.emit('commandMissingArgs', ctx, command) | ||||
|         return this.emit('commandMissingArgs', ctx) | ||||
|       else if ( | ||||
|         typeof command.args === 'number' && | ||||
|         parsed.args.length < command.args | ||||
|       ) | ||||
|         this.emit('commandMissingArgs', ctx, command) | ||||
|         this.emit('commandMissingArgs', ctx) | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       this.emit('commandUsed', ctx, command) | ||||
|       this.emit('commandUsed', ctx) | ||||
| 
 | ||||
|       const beforeExecute = await awaitSync(command.beforeExecute(ctx)) | ||||
|       if (beforeExecute === false) return | ||||
|  | @ -377,7 +367,7 @@ export class CommandClient extends Client implements CommandClientOptions { | |||
|       const result = await awaitSync(command.execute(ctx)) | ||||
|       command.afterExecute(ctx, result) | ||||
|     } catch (e) { | ||||
|       this.emit('commandError', command, ctx, e) | ||||
|       this.emit('commandError', ctx, e) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import { ClientEvents } from '../../mod.ts' | ||||
| import { Collection } from '../utils/collection.ts' | ||||
| import { Command } from './command.ts' | ||||
| import { CommandClient } from './commandClient.ts' | ||||
|  | @ -90,14 +91,14 @@ export class Extension { | |||
|       Object.keys(this._decoratedEvents).length !== 0 | ||||
|     ) { | ||||
|       Object.entries(this._decoratedEvents).forEach((entry) => { | ||||
|         this.listen(entry[0], entry[1]) | ||||
|         this.listen(entry[0] as keyof ClientEvents, entry[1]) | ||||
|       }) | ||||
|       this._decoratedEvents = undefined | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** Listens for an Event through Extension. */ | ||||
|   listen(event: string, cb: ExtensionEventCallback): boolean { | ||||
|   listen(event: keyof ClientEvents, cb: ExtensionEventCallback): boolean { | ||||
|     if (this.events[event] !== undefined) return false | ||||
|     else { | ||||
|       const fn = (...args: any[]): any => { | ||||
|  | @ -152,7 +153,7 @@ export class ExtensionsManager { | |||
|     if (extension === undefined) return false | ||||
|     extension.commands.deleteAll() | ||||
|     for (const [k, v] of Object.entries(extension.events)) { | ||||
|       this.client.removeListener(k, v) | ||||
|       this.client.off(k as keyof ClientEvents, v) | ||||
|       // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 | ||||
|       delete extension.events[k] | ||||
|     } | ||||
|  |  | |||
|  | @ -1,69 +1,75 @@ | |||
| import { Collection } from '../utils/collection.ts' | ||||
| import { Client, ClientOptions } from './client.ts' | ||||
| import {EventEmitter} from '../../deps.ts' | ||||
| import { Client } from './client.ts' | ||||
| import { RESTManager } from './rest.ts' | ||||
| // import { GATEWAY_BOT } from '../types/endpoint.ts'
 | ||||
| // import { GatewayBotPayload } from '../types/gatewayBot.ts'
 | ||||
| import { Gateway } from '../gateway/index.ts' | ||||
| import { HarmonyEventEmitter } from '../utils/events.ts' | ||||
| import { GatewayEvents } from '../types/gateway.ts' | ||||
| 
 | ||||
| // TODO(DjDeveloperr)
 | ||||
| // I'm kinda confused; will continue on this later once
 | ||||
| // Deno namespace in Web Worker is stable!
 | ||||
| export interface ShardManagerOptions { | ||||
|   client: Client | typeof Client | ||||
|   token?: string | ||||
|   intents?: number[] | ||||
|   options?: ClientOptions | ||||
|   shards: number | ||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||
| export type ShardManagerEvents = { | ||||
|   launch: [number] | ||||
|   shardReady: [number] | ||||
|   shardDisconnect: [number, number | undefined, string | undefined] | ||||
|   shardError: [number, Error, ErrorEvent] | ||||
|   shardResume: [number] | ||||
| } | ||||
| 
 | ||||
| export interface ShardManagerInitOptions { | ||||
|   file: string | ||||
|   token?: string | ||||
|   intents?: number[] | ||||
|   options?: ClientOptions | ||||
|   shards?: number | ||||
| } | ||||
| 
 | ||||
| export class ShardManager extends EventEmitter { | ||||
|   workers: Collection<string, Worker> = new Collection() | ||||
|   token: string | ||||
|   intents: number[] | ||||
|   shardCount: number | ||||
|   private readonly __client: Client | ||||
| export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> { | ||||
|   list: Collection<string, Gateway> = new Collection() | ||||
|   client: Client | ||||
|   cachedShardCount?: number | ||||
| 
 | ||||
|   get rest(): RESTManager { | ||||
|     return this.__client.rest | ||||
|     return this.client.rest | ||||
|   } | ||||
| 
 | ||||
|   constructor(options: ShardManagerOptions) { | ||||
|   constructor(client: Client) { | ||||
|     super() | ||||
|     this.__client = | ||||
|       options.client instanceof Client | ||||
|         ? options.client | ||||
|         : // eslint-disable-next-line new-cap
 | ||||
|           new options.client(options.options) | ||||
| 
 | ||||
|     if (this.__client.token === undefined || options.token === undefined) | ||||
|       throw new Error('Token should be provided when constructing ShardManager') | ||||
|     if (this.__client.intents === undefined || options.intents === undefined) | ||||
|       throw new Error( | ||||
|         'Intents should be provided when constructing ShardManager' | ||||
|       ) | ||||
| 
 | ||||
|     this.token = this.__client.token ?? options.token | ||||
|     this.intents = this.__client.intents ?? options.intents | ||||
|     this.shardCount = options.shards | ||||
|     this.client = client | ||||
|   } | ||||
| 
 | ||||
|   // static async init(): Promise<ShardManager> {}
 | ||||
|   async getShardCount(): Promise<number> { | ||||
|     let shardCount: number | ||||
|     if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount | ||||
|     else { | ||||
|       if (this.client.shardCount === 'auto') { | ||||
|         const info = await this.client.rest.api.gateway.bot.get() | ||||
|         shardCount = info.shards as number | ||||
|       } else shardCount = this.client.shardCount ?? 1 | ||||
|     } | ||||
|     this.cachedShardCount = shardCount | ||||
|     return this.cachedShardCount | ||||
|   } | ||||
| 
 | ||||
|   // async start(): Promise<ShardManager> {
 | ||||
|   //   const info = ((await this.rest.get(
 | ||||
|   //     GATEWAY_BOT()
 | ||||
|   //   )) as unknown) as GatewayBotPayload
 | ||||
|   /** Launches a new Shard */ | ||||
|   async launch(id: number): Promise<ShardManager> { | ||||
|     if (this.list.has(id.toString()) === true) | ||||
|       throw new Error(`Shard ${id} already launched`) | ||||
| 
 | ||||
|   //   const totalShards = this.__shardCount ?? info.shards
 | ||||
|     const shardCount = await this.getShardCount() | ||||
| 
 | ||||
|   //   return this
 | ||||
|   // }
 | ||||
|     const gw = new Gateway(this.client, [Number(id), shardCount]) | ||||
|     this.list.set(id.toString(), gw) | ||||
|     gw.initWebsocket() | ||||
|     this.emit('launch', id) | ||||
| 
 | ||||
|     gw.on(GatewayEvents.Ready, () => this.emit('shardReady', id)) | ||||
|     gw.on('error', (err: Error, evt: ErrorEvent) => | ||||
|       this.emit('shardError', id, err, evt) | ||||
|     ) | ||||
|     gw.on(GatewayEvents.Resumed, () => this.emit('shardResume', id)) | ||||
|     gw.on('close', (code: number, reason: string) => | ||||
|       this.emit('shardDisconnect', id, code, reason) | ||||
|     ) | ||||
| 
 | ||||
|     return gw.waitFor(GatewayEvents.Ready, () => true).then(() => this) | ||||
|   } | ||||
| 
 | ||||
|   async start(): Promise<ShardManager> { | ||||
|     const shardCount = await this.getShardCount() | ||||
|     for (let i = 0; i <= shardCount; i++) { | ||||
|       await this.launch(i) | ||||
|     } | ||||
|     return this | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -292,14 +292,14 @@ export class Guild extends Base { | |||
|         const listener = (guild: Guild): void => { | ||||
|           if (guild.id === this.id) { | ||||
|             chunked = true | ||||
|             this.client.removeListener('guildMembersChunked', listener) | ||||
|             this.client.off('guildMembersChunked', listener) | ||||
|             resolve(this) | ||||
|           } | ||||
|         } | ||||
|         this.client.on('guildMembersChunked', listener) | ||||
|         setTimeout(() => { | ||||
|           if (!chunked) { | ||||
|             this.client.removeListener('guildMembersChunked', listener) | ||||
|             this.client.off('guildMembersChunked', listener) | ||||
|           } | ||||
|         }, timeout) | ||||
|       } | ||||
|  | @ -312,19 +312,19 @@ export class Guild extends Base { | |||
|    */ | ||||
|   async awaitAvailability(timeout: number = 1000): Promise<Guild> { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       if(!this.unavailable) resolve(this); | ||||
|       if (!this.unavailable) resolve(this) | ||||
|       const listener = (guild: Guild): void => { | ||||
|         if (guild.id === this.id) { | ||||
|           this.client.removeListener('guildLoaded', listener); | ||||
|           resolve(this); | ||||
|           this.client.off('guildLoaded', listener) | ||||
|           resolve(this) | ||||
|         } | ||||
|       }; | ||||
|       this.client.on('guildLoaded', listener); | ||||
|       } | ||||
|       this.client.on('guildLoaded', listener) | ||||
|       setTimeout(() => { | ||||
|         this.client.removeListener('guildLoaded', listener); | ||||
|         reject(Error("Timeout. Guild didn't arrive in time.")); | ||||
|       }, timeout); | ||||
|     }); | ||||
|         this.client.off('guildLoaded', listener) | ||||
|         reject(Error("Timeout. Guild didn't arrive in time.")) | ||||
|       }, timeout) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -44,7 +44,7 @@ export class VoiceChannel extends Channel { | |||
|       const onVoiceStateAdd = (state: VoiceState): void => { | ||||
|         if (state.user.id !== this.client.user?.id) return | ||||
|         if (state.channel?.id !== this.id) return | ||||
|         this.client.removeListener('voiceStateAdd', onVoiceStateAdd) | ||||
|         this.client.off('voiceStateAdd', onVoiceStateAdd) | ||||
|         done++ | ||||
|         if (done >= 2) resolve((vcdata as unknown) as VoiceServerUpdateData) | ||||
|       } | ||||
|  | @ -52,7 +52,7 @@ export class VoiceChannel extends Channel { | |||
|       const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { | ||||
|         if (data.guild.id !== this.guild.id) return | ||||
|         vcdata = data | ||||
|         this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate) | ||||
|         this.client.off('voiceServerUpdate', onVoiceServerUpdate) | ||||
|         done++ | ||||
|         if (done >= 2) resolve(vcdata) | ||||
|       } | ||||
|  | @ -64,8 +64,8 @@ export class VoiceChannel extends Channel { | |||
| 
 | ||||
|       setTimeout(() => { | ||||
|         if (done < 2) { | ||||
|           this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate) | ||||
|           this.client.removeListener('voiceStateAdd', onVoiceStateAdd) | ||||
|           this.client.off('voiceServerUpdate', onVoiceServerUpdate) | ||||
|           this.client.off('voiceStateAdd', onVoiceStateAdd) | ||||
|           reject( | ||||
|             new Error( | ||||
|               "Connection timed out - couldn't connect to Voice Channel" | ||||
|  |  | |||
							
								
								
									
										30
									
								
								src/utils/events.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/utils/events.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import { EventEmitter } from '../../deps.ts' | ||||
| 
 | ||||
| export class HarmonyEventEmitter< | ||||
|   T extends Record<string, unknown[]> | ||||
| > extends EventEmitter<T> { | ||||
|   /** Wait for an Event to fire with given condition. */ | ||||
|   async waitFor<K extends keyof T>( | ||||
|     event: K, | ||||
|     checkFunction: (...args: T[K]) => boolean = () => true, | ||||
|     timeout?: number | ||||
|   ): Promise<T[K] | []> { | ||||
|     return await new Promise((resolve) => { | ||||
|       let timeoutID: number | undefined | ||||
|       if (timeout !== undefined) { | ||||
|         timeoutID = setTimeout(() => { | ||||
|           this.off(event, eventFunc) | ||||
|           resolve([]) | ||||
|         }, timeout) | ||||
|       } | ||||
|       const eventFunc = (...args: T[K]): void => { | ||||
|         if (checkFunction(...args)) { | ||||
|           resolve(args) | ||||
|           this.off(event, eventFunc) | ||||
|           if (timeoutID !== undefined) clearTimeout(timeoutID) | ||||
|         } | ||||
|       } | ||||
|       this.on(event, eventFunc) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue