Merge pull request #88 from DjDeveloperr/slash
BREAKING: Adds rest of the REST and migrate to Typed EventEmitter, better Gateway interfacing
This commit is contained in:
		
						commit
						b2a93769ff
					
				
					 19 changed files with 560 additions and 189 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' | ||||
|  |  | |||
							
								
								
									
										2
									
								
								mod.ts
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								mod.ts
									
										
									
									
									
								
							|  | @ -1,6 +1,7 @@ | |||
| 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' | ||||
|  | @ -124,3 +125,4 @@ 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' | ||||
|  |  | |||
|  | @ -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 { GuildChannels } from '../../types/guild.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: GuildChannels] | ||||
| 
 | ||||
|   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 | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -89,4 +89,20 @@ export class GuildChannelsManager extends BaseChildManager< | |||
|     const channel = await this.get(res.id) | ||||
|     return (channel as unknown) as GuildChannels | ||||
|   } | ||||
| 
 | ||||
|   /** Modify the positions of a set of channel positions for the guild. */ | ||||
|   async editPositions( | ||||
|     ...positions: Array<{ id: string | GuildChannels; position: number | null }> | ||||
|   ): Promise<GuildChannelsManager> { | ||||
|     if (positions.length === 0) | ||||
|       throw new Error('No channel positions to change specified') | ||||
| 
 | ||||
|     await this.client.rest.api.guilds[this.guild.id].channels.patch( | ||||
|       positions.map((e) => ({ | ||||
|         id: typeof e.id === 'string' ? e.id : e.id.id, | ||||
|         position: e.position ?? null | ||||
|       })) | ||||
|     ) | ||||
|     return this | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import { fetchAuto } from '../../deps.ts' | ||||
| import { Client } from '../models/client.ts' | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import { Template } from '../structures/template.ts' | ||||
| import { Role } from '../structures/role.ts' | ||||
| import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts' | ||||
| import { | ||||
|  | @ -16,7 +18,6 @@ import { | |||
| } from '../types/guild.ts' | ||||
| import { BaseManager } from './base.ts' | ||||
| import { MembersManager } from './members.ts' | ||||
| import { fetchAuto } from '../../deps.ts' | ||||
| import { Emoji } from '../structures/emoji.ts' | ||||
| 
 | ||||
| export class GuildManager extends BaseManager<GuildPayload, Guild> { | ||||
|  | @ -47,6 +48,19 @@ export class GuildManager extends BaseManager<GuildPayload, Guild> { | |||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** Create a new guild based on a template. */ | ||||
|   async createFromTemplate( | ||||
|     template: Template | string, | ||||
|     name: string, | ||||
|     icon?: string | ||||
|   ): Promise<Guild> { | ||||
|     if (icon?.startsWith('http') === true) icon = await fetchAuto(icon) | ||||
|     const guild = await this.client.rest.api.guilds.templates[ | ||||
|       typeof template === 'object' ? template.code : template | ||||
|     ].post({ name, icon }) | ||||
|     return new Guild(this.client, guild) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Creates a guild. Returns Guild. Fires guildCreate event. | ||||
|    * @param options Options for creating a guild | ||||
|  |  | |||
|  | @ -101,4 +101,20 @@ export class RolesManager extends BaseManager<RolePayload, Role> { | |||
| 
 | ||||
|     return new Role(this.client, resp, this.guild) | ||||
|   } | ||||
| 
 | ||||
|   /** Modify the positions of a set of role positions for the guild. */ | ||||
|   async editPositions( | ||||
|     ...positions: Array<{ id: string | Role; position: number | null }> | ||||
|   ): Promise<RolesManager> { | ||||
|     if (positions.length === 0) | ||||
|       throw new Error('No role positions to change specified') | ||||
| 
 | ||||
|     await this.client.rest.api.guilds[this.guild.id].roles.patch( | ||||
|       positions.map((e) => ({ | ||||
|         id: typeof e.id === 'string' ? e.id : e.id.id, | ||||
|         position: e.position ?? null | ||||
|       })) | ||||
|     ) | ||||
|     return this | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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,11 @@ 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 { | ||||
|  | @ -59,40 +63,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 +101,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 +134,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 +175,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 +189,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 | ||||
|     }) | ||||
| 
 | ||||
|     this.fetchGatewayInfo = options.fetchGatewayInfo ?? false | ||||
| 
 | ||||
|     if (this.token === undefined) { | ||||
|       try { | ||||
|         const token = Deno.env.get('DISCORD_TOKEN') | ||||
|  | @ -209,6 +220,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 +244,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 +284,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 +301,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 +345,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,32 +353,62 @@ 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) | ||||
|   /** 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) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -135,8 +142,8 @@ export class Collector extends EventEmitter { | |||
|   } | ||||
| 
 | ||||
|   /** Returns a Promise resolved when Collector ends or a timeout occurs */ | ||||
|   // eslint-disable-next-line
 | ||||
|   async wait(timeout: number = this.timeout ?? 0): Promise<Collector> { | ||||
|   async wait(timeout?: number): Promise<Collector> { | ||||
|     if (timeout === undefined) timeout = this.timeout ?? 0 | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
 | ||||
|       if (!timeout) | ||||
|  | @ -147,14 +154,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 | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { | |||
|   GuildFeatures, | ||||
|   GuildIntegrationPayload, | ||||
|   GuildPayload, | ||||
|   GuildWidgetPayload, | ||||
|   IntegrationAccountPayload, | ||||
|   IntegrationExpireBehavior, | ||||
|   Verification, | ||||
|  | @ -38,6 +39,8 @@ import { | |||
| import { GuildVoiceStatesManager } from '../managers/guildVoiceStates.ts' | ||||
| import { RequestMembersOptions } from '../gateway/index.ts' | ||||
| import { GuildPresencesManager } from '../managers/presences.ts' | ||||
| import { TemplatePayload } from '../types/template.ts' | ||||
| import { Template } from './template.ts' | ||||
| 
 | ||||
| export class GuildBan extends Base { | ||||
|   guild: Guild | ||||
|  | @ -328,18 +331,137 @@ export class Guild extends Base { | |||
|       if (!this.unavailable) resolve(this) | ||||
|       const listener = (guild: Guild): void => { | ||||
|         if (guild.id === this.id) { | ||||
|           this.client.removeListener('guildLoaded', listener) | ||||
|           this.client.off('guildLoaded', listener) | ||||
|           resolve(this) | ||||
|         } | ||||
|       } | ||||
|       this.client.on('guildLoaded', listener) | ||||
|       setTimeout(() => { | ||||
|         this.client.removeListener('guildLoaded', listener) | ||||
|         this.client.off('guildLoaded', listener) | ||||
|         reject(Error("Timeout. Guild didn't arrive in time.")) | ||||
|       }, timeout) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** Attach an integration object from the current user to the guild. */ | ||||
|   async createIntegration(id: string, type: string): Promise<Guild> { | ||||
|     await this.client.rest.api.guilds[this.id].integrations.post({ id, type }) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Modify the behavior and settings of an integration object for the guild. */ | ||||
|   async editIntegration( | ||||
|     id: string, | ||||
|     data: { | ||||
|       expireBehavior?: number | null | ||||
|       expireGracePeriod?: number | null | ||||
|       enableEmoticons?: boolean | null | ||||
|     } | ||||
|   ): Promise<Guild> { | ||||
|     await this.client.rest.api.guilds[this.id].integrations[id].patch({ | ||||
|       expire_behaviour: data.expireBehavior, | ||||
|       expire_grace_period: data.expireGracePeriod, | ||||
|       enable_emoticons: data.enableEmoticons | ||||
|     }) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Delete the attached integration object for the guild. Deletes any associated webhooks and kicks the associated bot if there is one. */ | ||||
|   async deleteIntegration(id: string): Promise<Guild> { | ||||
|     await this.client.rest.api.guilds[this.id].integrations[id].delete() | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Sync an integration. */ | ||||
|   async syncIntegration(id: string): Promise<Guild> { | ||||
|     await this.client.rest.api.guilds[this.id].integrations[id].sync.post() | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Returns the widget for the guild. */ | ||||
|   async getWidget(): Promise<GuildWidgetPayload> { | ||||
|     return this.client.rest.api.guilds[this.id]['widget.json'].get() | ||||
|   } | ||||
| 
 | ||||
|   /** Modify a guild widget object for the guild. */ | ||||
|   async editWidget(data: { | ||||
|     enabled?: boolean | ||||
|     channel?: string | GuildChannels | ||||
|   }): Promise<Guild> { | ||||
|     await this.client.rest.api.guilds[this.id].widget.patch({ | ||||
|       enabled: data.enabled, | ||||
|       channel_id: | ||||
|         typeof data.channel === 'object' ? data.channel.id : data.channel | ||||
|     }) | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Returns a partial invite object for guilds with that feature enabled. */ | ||||
|   async getVanity(): Promise<{ code: string | null; uses: number }> { | ||||
|     return this.client.rest.api.guilds[this.id]['vanity-url'].get() | ||||
|   } | ||||
| 
 | ||||
|   /** Returns a PNG (URL) image widget for the guild. */ | ||||
|   getWidgetImageURL( | ||||
|     style?: 'shield' | 'banner1' | 'banner2' | 'banner3' | 'banner4' | ||||
|   ): string { | ||||
|     return `https://discord.com/api/v${this.client.rest.version ?? 8}/guilds/${ | ||||
|       this.id | ||||
|     }/widget.png${style !== undefined ? `?style=${style}` : ''}` | ||||
|   } | ||||
| 
 | ||||
|   /** Leave a Guild. */ | ||||
|   async leave(): Promise<Client> { | ||||
|     await this.client.rest.api.users['@me'].guilds[this.id].delete() | ||||
|     return this.client | ||||
|   } | ||||
| 
 | ||||
|   /** Returns an array of template objects. */ | ||||
|   async getTemplates(): Promise<Template[]> { | ||||
|     return this.client.rest.api.guilds[this.id].templates | ||||
|       .get() | ||||
|       .then((temps: TemplatePayload[]) => | ||||
|         temps.map((temp) => new Template(this.client, temp)) | ||||
|       ) | ||||
|   } | ||||
| 
 | ||||
|   /** Creates a template for the guild. */ | ||||
|   async createTemplate( | ||||
|     name: string, | ||||
|     description?: string | null | ||||
|   ): Promise<Template> { | ||||
|     const payload = await this.client.rest.api.guilds[this.id].templates.post({ | ||||
|       name, | ||||
|       description | ||||
|     }) | ||||
|     return new Template(this.client, payload) | ||||
|   } | ||||
| 
 | ||||
|   /** Syncs the template to the guild's current state. */ | ||||
|   async syncTemplate(code: string): Promise<Template> { | ||||
|     const payload = await this.client.rest.api.guilds[this.id].templates[ | ||||
|       code | ||||
|     ].sync.put() | ||||
|     return new Template(this.client, payload) | ||||
|   } | ||||
| 
 | ||||
|   /** Modifies the template's metadata. */ | ||||
|   async editTemplate( | ||||
|     code: string, | ||||
|     data: { name?: string; description?: string } | ||||
|   ): Promise<Template> { | ||||
|     const payload = await this.client.rest.api.guilds[this.id].templates[ | ||||
|       code | ||||
|     ].patch({ name: data.name, description: data.description }) | ||||
|     return new Template(this.client, payload) | ||||
|   } | ||||
| 
 | ||||
|   /** Deletes the template. Requires the MANAGE_GUILD permission. */ | ||||
|   async deleteTemplate(code: string): Promise<Guild> { | ||||
|     await this.client.rest.api.guilds[this.id].templates[code].delete() | ||||
|     return this | ||||
|   } | ||||
| 
 | ||||
|   /** Gets a preview of the guild. Returns GuildPreview. */ | ||||
|   async preview(): Promise<GuildPreview> { | ||||
|     return this.client.guilds.preview(this.id) | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -162,6 +162,15 @@ export interface GuildBanPayload { | |||
|   user: UserPayload | ||||
| } | ||||
| 
 | ||||
| export interface GuildWidgetPayload { | ||||
|   id: string | ||||
|   name: string | ||||
|   instant_invite: string | ||||
|   channels: Array<{ id: string; name: string; position: number }> | ||||
|   members: MemberPayload[] | ||||
|   presence_count: number | ||||
| } | ||||
| 
 | ||||
| export type GuildChannelPayloads = | ||||
|   | GuildTextChannelPayload | ||||
|   | GuildVoiceChannelPayload | ||||
|  |  | |||
|  | @ -42,3 +42,19 @@ export interface VoiceStatePayload { | |||
|   self_video: boolean | ||||
|   suppress: boolean | ||||
| } | ||||
| 
 | ||||
| /** Voice Region Structure */ | ||||
| export interface VoiceRegion { | ||||
|   /** Unique ID for the region */ | ||||
|   id: string | ||||
|   /** Name of the region */ | ||||
|   name: string | ||||
|   /** True if this is a vip-only server */ | ||||
|   vip: boolean | ||||
|   /** True for a single server that is closest to the current user's client */ | ||||
|   optimal: boolean | ||||
|   /** Whether this is a deprecated voice region (avoid switching to these) */ | ||||
|   deprecated: boolean | ||||
|   /** Whether this is a custom voice region (used for events/etc) */ | ||||
|   custom: boolean | ||||
| } | ||||
|  |  | |||
							
								
								
									
										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