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