feat: port to typed EventEmitter

This commit is contained in:
DjDeveloperr 2021-01-20 15:35:15 +05:30
parent 3f436b2b3f
commit e7b0804616
13 changed files with 318 additions and 198 deletions

View file

@ -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 { 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 { 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' export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'

View file

@ -1,5 +1,9 @@
import { GatewayEventHandler } from '../index.ts' 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 { channelCreate } from './channelCreate.ts'
import { channelDelete } from './channelDelete.ts' import { channelDelete } from './channelDelete.ts'
import { channelUpdate } from './channelUpdate.ts' import { channelUpdate } from './channelUpdate.ts'
@ -55,6 +59,10 @@ import {
} from '../../utils/getChannelByType.ts' } from '../../utils/getChannelByType.ts'
import { interactionCreate } from './interactionCreate.ts' import { interactionCreate } from './interactionCreate.ts'
import { Interaction } from '../../structures/slash.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: { export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined [eventCode in GatewayEvents]: GatewayEventHandler | undefined
@ -105,7 +113,8 @@ export interface VoiceServerUpdateData {
guild: Guild guild: Guild
} }
export interface ClientEvents { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ClientEvents = {
/** When Client has successfully connected to Discord */ /** When Client has successfully connected to Discord */
ready: [] ready: []
/** When a successful reconnect has been made */ /** When a successful reconnect has been made */
@ -355,4 +364,40 @@ export interface ClientEvents {
* @param payload Payload JSON of the event * @param payload Payload JSON of the event
*/ */
raw: [evt: string, payload: any] 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]
} }

View file

@ -7,6 +7,7 @@ export const ready: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,
d: Ready d: Ready
) => { ) => {
gateway.client.upSince = new Date()
await gateway.client.guilds.flush() await gateway.client.guilds.flush()
await gateway.client.users.set(d.user.id, d.user) await gateway.client.users.set(d.user.id, d.user)

View file

@ -8,7 +8,7 @@ export const resume: GatewayEventHandler = async (
d: Resume d: Resume
) => { ) => {
gateway.debug(`Session Resumed!`) gateway.debug(`Session Resumed!`)
gateway.client.emit('resume') gateway.client.emit('resumed')
if (gateway.client.user === undefined) if (gateway.client.user === undefined)
gateway.client.user = new User( gateway.client.user = new User(
gateway.client, gateway.client,

View file

@ -1,4 +1,4 @@
import { unzlib, EventEmitter } from '../../deps.ts' import { unzlib } from '../../deps.ts'
import { Client } from '../models/client.ts' import { Client } from '../models/client.ts'
import { import {
DISCORD_GATEWAY_URL, DISCORD_GATEWAY_URL,
@ -10,14 +10,15 @@ import {
GatewayIntents, GatewayIntents,
GatewayCloseCodes, GatewayCloseCodes,
IdentityPayload, IdentityPayload,
StatusUpdatePayload StatusUpdatePayload,
GatewayEvents
} from '../types/gateway.ts' } from '../types/gateway.ts'
import { gatewayHandlers } from './handlers/index.ts' import { gatewayHandlers } from './handlers/index.ts'
import { GATEWAY_BOT } from '../types/endpoint.ts'
import { GatewayCache } from '../managers/gatewayCache.ts' import { GatewayCache } from '../managers/gatewayCache.ts'
import { delay } from '../utils/delay.ts' import { delay } from '../utils/delay.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts' import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import { Guild } from '../structures/guild.ts' import { Guild } from '../structures/guild.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
export interface RequestMembersOptions { export interface RequestMembersOptions {
limit?: number limit?: number
@ -33,15 +34,31 @@ export interface VoiceStateOptions {
export const RECONNECT_REASON = 'harmony-reconnect' 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. * Handles Discord Gateway connection.
* *
* You should not use this and rather use Client class. * You should not use this and rather use Client class.
*/ */
export class Gateway extends EventEmitter { export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
websocket: WebSocket websocket?: WebSocket
token: string token?: string
intents: GatewayIntents[] intents?: GatewayIntents[]
connected = false connected = false
initialized = false initialized = false
heartbeatInterval = 0 heartbeatInterval = 0
@ -53,23 +70,13 @@ export class Gateway extends EventEmitter {
client: Client client: Client
cache: GatewayCache cache: GatewayCache
private timedIdentify: number | null = null private timedIdentify: number | null = null
shards?: number[]
constructor(client: Client, token: string, intents: GatewayIntents[]) { constructor(client: Client, shards?: number[]) {
super() super()
this.token = token
this.intents = intents
this.client = client this.client = client
this.cache = new GatewayCache(client) this.cache = new GatewayCache(client)
this.websocket = new WebSocket( this.shards = shards
// 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)
} }
private onopen(): void { private onopen(): void {
@ -145,7 +152,7 @@ export class Gateway extends EventEmitter {
await this.cache.set('seq', s) await this.cache.set('seq', s)
} }
if (t !== null && t !== undefined) { if (t !== null && t !== undefined) {
this.emit(t, d) this.emit(t as any, d)
this.client.emit('raw', t, d) this.client.emit('raw', t, d)
const handler = gatewayHandlers[t] const handler = gatewayHandlers[t]
@ -236,24 +243,40 @@ export class Gateway extends EventEmitter {
} }
} }
private onerror(event: Event | ErrorEvent): void { private async onerror(event: ErrorEvent): Promise<void> {
const eventError = event as ErrorEvent const error = new Error(
this.emit('error', eventError) 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> { private async sendIdentify(forceNewSession?: boolean): Promise<void> {
this.debug('Fetching /gateway/bot...') if (typeof this.token !== 'string') throw new Error('Token not specified')
const info = await this.client.rest.get(GATEWAY_BOT()) if (typeof this.intents !== 'object')
if (info.session_start_limit.remaining === 0) throw new Error('Intents not specified')
throw new Error(
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms` 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(`Reset After: ${info.session_start_limit.reset_after}ms`)
this.debug('=== Session Limit Info ===') }
this.debug(
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
)
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
if (forceNewSession === undefined || !forceNewSession) { if (forceNewSession === undefined || !forceNewSession) {
const sessionIDCached = await this.cache.get('session_id') const sessionIDCached = await this.cache.get('session_id')
@ -272,7 +295,10 @@ export class Gateway extends EventEmitter {
$device: this.client.clientProperties.device ?? 'harmony' $device: this.client.clientProperties.device ?? 'harmony'
}, },
compress: true, 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( intents: this.intents.reduce(
(previous, current) => previous | current, (previous, current) => previous | current,
0 0
@ -289,6 +315,10 @@ export class Gateway extends EventEmitter {
} }
private async sendResume(): Promise<void> { 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) { if (this.sessionID === undefined) {
this.sessionID = await this.cache.get('session_id') this.sessionID = await this.cache.get('session_id')
if (this.sessionID === undefined) return await this.sendIdentify() 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.onopen = this.onopen.bind(this)
this.websocket.onmessage = this.onmessage.bind(this) this.websocket.onmessage = this.onmessage.bind(this)
this.websocket.onclose = this.onclose.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 { close(code: number = 1000, reason?: string): void {
return this.websocket.close(code, reason) return this.websocket?.close(code, reason)
} }
send(data: GatewayResponse): boolean { 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({ const packet = JSON.stringify({
op: data.op, op: data.op,
d: data.d, d: data.d,
s: typeof data.s === 'number' ? data.s : null, s: typeof data.s === 'number' ? data.s : null,
t: data.t === undefined ? null : data.t t: data.t === undefined ? null : data.t
}) })
this.websocket.send(packet) this.websocket?.send(packet)
return true return true
} }

View file

@ -3,7 +3,6 @@ import { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts' import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts' import { Gateway } from '../gateway/index.ts'
import { RESTManager, RESTOptions, TokenType } from './rest.ts' import { RESTManager, RESTOptions, TokenType } from './rest.ts'
import { EventEmitter } from '../../deps.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UsersManager } from '../managers/users.ts' import { UsersManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts' import { GuildManager } from '../managers/guilds.ts'
@ -21,6 +20,7 @@ import { Invite } from '../structures/invite.ts'
import { INVITE } from '../types/endpoint.ts' import { INVITE } from '../types/endpoint.ts'
import { ClientEvents } from '../gateway/handlers/index.ts' import { ClientEvents } from '../gateway/handlers/index.ts'
import type { Collector } from './collectors.ts' import type { Collector } from './collectors.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
/** OS related properties sent with Gateway Identify */ /** OS related properties sent with Gateway Identify */
export interface ClientProperties { export interface ClientProperties {
@ -59,40 +59,20 @@ export interface ClientOptions {
disableEnvToken?: boolean disableEnvToken?: boolean
/** Override REST Options */ /** Override REST Options */
restOptions?: RESTOptions restOptions?: RESTOptions
} /** Whether to fetch Gateway info or not */
fetchGatewayInfo?: boolean
export declare interface Client { /** ADVANCED: Shard ID to launch on */
on<K extends keyof ClientEvents>( shard?: number
event: K, /** Shard count. Set to 'auto' for automatic sharding */
listener: (...args: ClientEvents[K]) => void shardCount?: number | 'auto'
): 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
} }
/** /**
* Discord Client. * Discord Client.
*/ */
export class Client extends EventEmitter { export class Client extends HarmonyEventEmitter<ClientEvents> {
/** Gateway object */ /** Gateway object */
gateway?: Gateway gateway: Gateway
/** REST Manager - used to make all requests */ /** REST Manager - used to make all requests */
rest: RESTManager rest: RESTManager
/** User which Client logs in to, undefined until logs in */ /** User which Client logs in to, undefined until logs in */
@ -117,12 +97,21 @@ export class Client extends EventEmitter {
clientProperties: ClientProperties clientProperties: ClientProperties
/** Slash-Commands Management client */ /** Slash-Commands Management client */
slash: SlashClient slash: SlashClient
/** Whether to fetch Gateway info or not */
fetchGatewayInfo: boolean = true
/** Users Manager, containing all Users cached */
users: UsersManager = new UsersManager(this) users: UsersManager = new UsersManager(this)
/** Guilds Manager, providing cache & API interface to Guilds */
guilds: GuildManager = new GuildManager(this) guilds: GuildManager = new GuildManager(this)
/** Channels Manager, providing cache interface to Channels */
channels: ChannelsManager = new ChannelsManager(this) channels: ChannelsManager = new ChannelsManager(this)
/** Channels Manager, providing cache interface to Channels */
emojis: EmojisManager = new EmojisManager(this) emojis: EmojisManager = new EmojisManager(this)
/** Last READY timestamp */
upSince?: Date
/** Client's presence. Startup one if set before connecting */ /** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence() presence: ClientPresence = new ClientPresence()
_decoratedEvents?: { _decoratedEvents?: {
@ -141,10 +130,23 @@ export class Client extends EventEmitter {
/** Shard on which this Client is */ /** Shard on which this Client is */
shard: number = 0 shard: number = 0
/** Shard Count */
shardCount: number | 'auto' = 1
/** Shard Manager of this Client if Sharded */ /** Shard Manager of this Client if Sharded */
shardManager?: ShardManager shards?: ShardManager
/** Collectors set */
collectors: Set<Collector> = new 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 = {}) { constructor(options: ClientOptions = {}) {
super() super()
this._id = options.id this._id = options.id
@ -169,7 +171,7 @@ export class Client extends EventEmitter {
Object.keys(this._decoratedEvents).length !== 0 Object.keys(this._decoratedEvents).length !== 0
) { ) {
Object.entries(this._decoratedEvents).forEach((entry) => { Object.entries(this._decoratedEvents).forEach((entry) => {
this.on(entry[0], entry[1]) this.on(entry[0] as keyof ClientEvents, entry[1])
}) })
this._decoratedEvents = undefined this._decoratedEvents = undefined
} }
@ -183,12 +185,17 @@ export class Client extends EventEmitter {
} }
: options.clientProperties : options.clientProperties
if (options.shard !== undefined) this.shard = options.shard
if (options.shardCount !== undefined) this.shardCount = options.shardCount
this.slash = new SlashClient({ this.slash = new SlashClient({
id: () => this.getEstimatedID(), id: () => this.getEstimatedID(),
client: this, client: this,
enabled: options.enableSlash enabled: options.enableSlash
}) })
if (options.fetchGatewayInfo === true) this.fetchGatewayInfo = true
if (this.token === undefined) { if (this.token === undefined) {
try { try {
const token = Deno.env.get('DISCORD_TOKEN') const token = Deno.env.get('DISCORD_TOKEN')
@ -209,6 +216,7 @@ export class Client extends EventEmitter {
if (options.restOptions !== undefined) if (options.restOptions !== undefined)
Object.assign(restOptions, options.restOptions) Object.assign(restOptions, options.restOptions)
this.rest = new RESTManager(restOptions) this.rest = new RESTManager(restOptions)
this.gateway = new Gateway(this)
} }
/** /**
@ -232,6 +240,7 @@ export class Client extends EventEmitter {
/** Emits debug event */ /** Emits debug event */
debug(tag: string, msg: string): void { debug(tag: string, msg: string): void {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.emit('debug', `[${tag}] ${msg}`) 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 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. * @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 if (token === undefined && this.token !== undefined) token = this.token
else if (this.token === undefined && token !== undefined) { else if (this.token === undefined && token !== undefined) {
this.token = token this.token = token
@ -288,7 +297,30 @@ export class Client extends EventEmitter {
} else throw new Error('No Gateway Intents were provided') } else throw new Error('No Gateway Intents were provided')
this.rest.token = token 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 */ /** 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[] = [] const collectors: Collector[] = []
for (const collector of this.collectors.values()) { for (const collector of this.collectors.values()) {
if (collector.event === event) collectors.push(collector) if (collector.event === event) collectors.push(collector)
@ -317,33 +349,11 @@ export class Client extends EventEmitter {
if (collectors.length !== 0) { if (collectors.length !== 0) {
this.collectors.forEach((collector) => collector._fire(...args)) 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) 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 */ /** Event decorator to create an Event handler from function */

View file

@ -1,6 +1,6 @@
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { EventEmitter } from '../../deps.ts'
import type { Client } from './client.ts' import type { Client } from './client.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean> export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
@ -19,7 +19,14 @@ export interface CollectorOptions {
timeout?: number 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 client?: Client
private _started: boolean = false private _started: boolean = false
event: string event: string
@ -146,14 +153,14 @@ export class Collector extends EventEmitter {
let done = false let done = false
const onend = (): void => { const onend = (): void => {
done = true done = true
this.removeListener('end', onend) this.off('end', onend)
resolve(this) resolve(this)
} }
this.on('end', onend) this.on('end', onend)
setTimeout(() => { setTimeout(() => {
if (!done) { if (!done) {
this.removeListener('end', onend) this.off('end', onend)
reject(new Error('Timeout')) reject(new Error('Timeout'))
} }
}, timeout) }, timeout)

View file

@ -259,7 +259,7 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.ownerOnly) === true && : category.ownerOnly) === true &&
!this.owners.includes(msg.author.id) !this.owners.includes(msg.author.id)
) )
return this.emit('commandOwnerOnly', ctx, command) return this.emit('commandOwnerOnly', ctx)
// Checks if Command is only for Guild // Checks if Command is only for Guild
if ( if (
@ -268,7 +268,7 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.guildOnly) === true && : category.guildOnly) === true &&
msg.guild === undefined msg.guild === undefined
) )
return this.emit('commandGuildOnly', ctx, command) return this.emit('commandGuildOnly', ctx)
// Checks if Command is only for DMs // Checks if Command is only for DMs
if ( if (
@ -277,14 +277,14 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.dmOnly) === true && : category.dmOnly) === true &&
msg.guild !== undefined msg.guild !== undefined
) )
return this.emit('commandDmOnly', ctx, command) return this.emit('commandDmOnly', ctx)
if ( if (
command.nsfw === true && command.nsfw === true &&
(msg.guild === undefined || (msg.guild === undefined ||
((msg.channel as unknown) as GuildTextChannel).nsfw !== true) ((msg.channel as unknown) as GuildTextChannel).nsfw !== true)
) )
return this.emit('commandNSFW', ctx, command) return this.emit('commandNSFW', ctx)
const allPermissions = const allPermissions =
command.permissions !== undefined command.permissions !== undefined
@ -316,12 +316,7 @@ export class CommandClient extends Client implements CommandClientOptions {
} }
if (missing.length !== 0) if (missing.length !== 0)
return this.emit( return this.emit('commandBotMissingPermissions', ctx, missing)
'commandBotMissingPermissions',
ctx,
command,
missing
)
} }
} }
@ -349,27 +344,22 @@ export class CommandClient extends Client implements CommandClientOptions {
} }
if (missing.length !== 0) if (missing.length !== 0)
return this.emit( return this.emit('commandUserMissingPermissions', ctx, missing)
'commandUserMissingPermissions',
command,
missing,
ctx
)
} }
} }
if (command.args !== undefined) { if (command.args !== undefined) {
if (typeof command.args === 'boolean' && parsed.args.length === 0) if (typeof command.args === 'boolean' && parsed.args.length === 0)
return this.emit('commandMissingArgs', ctx, command) return this.emit('commandMissingArgs', ctx)
else if ( else if (
typeof command.args === 'number' && typeof command.args === 'number' &&
parsed.args.length < command.args parsed.args.length < command.args
) )
this.emit('commandMissingArgs', ctx, command) this.emit('commandMissingArgs', ctx)
} }
try { try {
this.emit('commandUsed', ctx, command) this.emit('commandUsed', ctx)
const beforeExecute = await awaitSync(command.beforeExecute(ctx)) const beforeExecute = await awaitSync(command.beforeExecute(ctx))
if (beforeExecute === false) return if (beforeExecute === false) return
@ -377,7 +367,7 @@ export class CommandClient extends Client implements CommandClientOptions {
const result = await awaitSync(command.execute(ctx)) const result = await awaitSync(command.execute(ctx))
command.afterExecute(ctx, result) command.afterExecute(ctx, result)
} catch (e) { } catch (e) {
this.emit('commandError', command, ctx, e) this.emit('commandError', ctx, e)
} }
} }
} }

View file

@ -1,3 +1,4 @@
import { ClientEvents } from '../../mod.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { Command } from './command.ts' import { Command } from './command.ts'
import { CommandClient } from './commandClient.ts' import { CommandClient } from './commandClient.ts'
@ -90,14 +91,14 @@ export class Extension {
Object.keys(this._decoratedEvents).length !== 0 Object.keys(this._decoratedEvents).length !== 0
) { ) {
Object.entries(this._decoratedEvents).forEach((entry) => { Object.entries(this._decoratedEvents).forEach((entry) => {
this.listen(entry[0], entry[1]) this.listen(entry[0] as keyof ClientEvents, entry[1])
}) })
this._decoratedEvents = undefined this._decoratedEvents = undefined
} }
} }
/** Listens for an Event through Extension. */ /** 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 if (this.events[event] !== undefined) return false
else { else {
const fn = (...args: any[]): any => { const fn = (...args: any[]): any => {
@ -152,7 +153,7 @@ export class ExtensionsManager {
if (extension === undefined) return false if (extension === undefined) return false
extension.commands.deleteAll() extension.commands.deleteAll()
for (const [k, v] of Object.entries(extension.events)) { 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 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete extension.events[k] delete extension.events[k]
} }

View file

@ -1,69 +1,75 @@
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { Client, ClientOptions } from './client.ts' import { Client } from './client.ts'
import {EventEmitter} from '../../deps.ts'
import { RESTManager } from './rest.ts' import { RESTManager } from './rest.ts'
// import { GATEWAY_BOT } from '../types/endpoint.ts' import { Gateway } from '../gateway/index.ts'
// import { GatewayBotPayload } from '../types/gatewayBot.ts' import { HarmonyEventEmitter } from '../utils/events.ts'
import { GatewayEvents } from '../types/gateway.ts'
// TODO(DjDeveloperr) // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
// I'm kinda confused; will continue on this later once export type ShardManagerEvents = {
// Deno namespace in Web Worker is stable! launch: [number]
export interface ShardManagerOptions { shardReady: [number]
client: Client | typeof Client shardDisconnect: [number, number | undefined, string | undefined]
token?: string shardError: [number, Error, ErrorEvent]
intents?: number[] shardResume: [number]
options?: ClientOptions
shards: number
} }
export interface ShardManagerInitOptions { export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
file: string list: Collection<string, Gateway> = new Collection()
token?: string client: Client
intents?: number[] cachedShardCount?: 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
get rest(): RESTManager { get rest(): RESTManager {
return this.__client.rest return this.client.rest
} }
constructor(options: ShardManagerOptions) { constructor(client: Client) {
super() super()
this.__client = this.client = 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
} }
// 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> { /** Launches a new Shard */
// const info = ((await this.rest.get( async launch(id: number): Promise<ShardManager> {
// GATEWAY_BOT() if (this.list.has(id.toString()) === true)
// )) as unknown) as GatewayBotPayload 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
}
} }

View file

@ -292,14 +292,14 @@ export class Guild extends Base {
const listener = (guild: Guild): void => { const listener = (guild: Guild): void => {
if (guild.id === this.id) { if (guild.id === this.id) {
chunked = true chunked = true
this.client.removeListener('guildMembersChunked', listener) this.client.off('guildMembersChunked', listener)
resolve(this) resolve(this)
} }
} }
this.client.on('guildMembersChunked', listener) this.client.on('guildMembersChunked', listener)
setTimeout(() => { setTimeout(() => {
if (!chunked) { if (!chunked) {
this.client.removeListener('guildMembersChunked', listener) this.client.off('guildMembersChunked', listener)
} }
}, timeout) }, timeout)
} }
@ -312,19 +312,19 @@ export class Guild extends Base {
*/ */
async awaitAvailability(timeout: number = 1000): Promise<Guild> { async awaitAvailability(timeout: number = 1000): Promise<Guild> {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
if(!this.unavailable) resolve(this); if (!this.unavailable) resolve(this)
const listener = (guild: Guild): void => { const listener = (guild: Guild): void => {
if (guild.id === this.id) { if (guild.id === this.id) {
this.client.removeListener('guildLoaded', listener); this.client.off('guildLoaded', listener)
resolve(this); resolve(this)
} }
}; }
this.client.on('guildLoaded', listener); this.client.on('guildLoaded', listener)
setTimeout(() => { setTimeout(() => {
this.client.removeListener('guildLoaded', listener); this.client.off('guildLoaded', listener)
reject(Error("Timeout. Guild didn't arrive in time.")); reject(Error("Timeout. Guild didn't arrive in time."))
}, timeout); }, timeout)
}); })
} }
} }

View file

@ -44,7 +44,7 @@ export class VoiceChannel extends Channel {
const onVoiceStateAdd = (state: VoiceState): void => { const onVoiceStateAdd = (state: VoiceState): void => {
if (state.user.id !== this.client.user?.id) return if (state.user.id !== this.client.user?.id) return
if (state.channel?.id !== this.id) return if (state.channel?.id !== this.id) return
this.client.removeListener('voiceStateAdd', onVoiceStateAdd) this.client.off('voiceStateAdd', onVoiceStateAdd)
done++ done++
if (done >= 2) resolve((vcdata as unknown) as VoiceServerUpdateData) if (done >= 2) resolve((vcdata as unknown) as VoiceServerUpdateData)
} }
@ -52,7 +52,7 @@ export class VoiceChannel extends Channel {
const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => {
if (data.guild.id !== this.guild.id) return if (data.guild.id !== this.guild.id) return
vcdata = data vcdata = data
this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate) this.client.off('voiceServerUpdate', onVoiceServerUpdate)
done++ done++
if (done >= 2) resolve(vcdata) if (done >= 2) resolve(vcdata)
} }
@ -64,8 +64,8 @@ export class VoiceChannel extends Channel {
setTimeout(() => { setTimeout(() => {
if (done < 2) { if (done < 2) {
this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate) this.client.off('voiceServerUpdate', onVoiceServerUpdate)
this.client.removeListener('voiceStateAdd', onVoiceStateAdd) this.client.off('voiceStateAdd', onVoiceStateAdd)
reject( reject(
new Error( new Error(
"Connection timed out - couldn't connect to Voice Channel" "Connection timed out - couldn't connect to Voice Channel"

30
src/utils/events.ts Normal file
View 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)
})
}
}