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 { 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'
|
||||||
|
|
2
mod.ts
2
mod.ts
|
@ -1,6 +1,7 @@
|
||||||
export { GatewayIntents } from './src/types/gateway.ts'
|
export { GatewayIntents } from './src/types/gateway.ts'
|
||||||
export { Base } from './src/structures/base.ts'
|
export { Base } from './src/structures/base.ts'
|
||||||
export { Gateway } from './src/gateway/index.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 type { ClientEvents } from './src/gateway/handlers/index.ts'
|
||||||
export * from './src/models/client.ts'
|
export * from './src/models/client.ts'
|
||||||
export * from './src/models/slashClient.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 { UserFlags } from './src/types/userFlags.ts'
|
||||||
export type { VoiceStatePayload } from './src/types/voice.ts'
|
export type { VoiceStatePayload } from './src/types/voice.ts'
|
||||||
export type { WebhookPayload } from './src/types/webhook.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 { 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 { GuildChannels } from '../../types/guild.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: 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,
|
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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,14 +243,29 @@ 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> {
|
||||||
|
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...')
|
this.debug('Fetching /gateway/bot...')
|
||||||
const info = await this.client.rest.get(GATEWAY_BOT())
|
const info = await this.client.rest.api.gateway.bot.get()
|
||||||
if (info.session_start_limit.remaining === 0)
|
if (info.session_start_limit.remaining === 0)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms`
|
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms`
|
||||||
|
@ -254,6 +276,7 @@ export class Gateway extends EventEmitter {
|
||||||
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
|
`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) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,4 +89,20 @@ export class GuildChannelsManager extends BaseChildManager<
|
||||||
const channel = await this.get(res.id)
|
const channel = await this.get(res.id)
|
||||||
return (channel as unknown) as GuildChannels
|
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 { Client } from '../models/client.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { Guild } from '../structures/guild.ts'
|
||||||
|
import { Template } from '../structures/template.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
|
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
|
||||||
import {
|
import {
|
||||||
|
@ -16,7 +18,6 @@ import {
|
||||||
} from '../types/guild.ts'
|
} from '../types/guild.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
import { MembersManager } from './members.ts'
|
import { MembersManager } from './members.ts'
|
||||||
import { fetchAuto } from '../../deps.ts'
|
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
|
|
||||||
export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
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.
|
* Creates a guild. Returns Guild. Fires guildCreate event.
|
||||||
* @param options Options for creating a guild
|
* @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)
|
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 { 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,11 @@ 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'
|
||||||
|
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 */
|
/** OS related properties sent with Gateway Identify */
|
||||||
export interface ClientProperties {
|
export interface ClientProperties {
|
||||||
|
@ -59,40 +63,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 +101,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 +134,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 +175,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 +189,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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.fetchGatewayInfo = options.fetchGatewayInfo ?? false
|
||||||
|
|
||||||
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 +220,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 +244,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 +284,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 +301,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 +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[] = []
|
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,32 +353,62 @@ 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 */
|
/** Returns an array of voice region objects that can be used when creating servers. */
|
||||||
async waitFor<K extends keyof ClientEvents>(
|
async fetchVoiceRegions(): Promise<VoiceRegion[]> {
|
||||||
event: K,
|
return this.rest.api.voice.regions.get()
|
||||||
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)) {
|
/** Modify current (Client) User. */
|
||||||
resolve(args)
|
async editUser(data: {
|
||||||
this.off(event, eventFunc)
|
username?: string
|
||||||
if (timeoutID !== undefined) clearTimeout(timeoutID)
|
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)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.on(event, eventFunc)
|
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 { 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
|
||||||
|
@ -135,8 +142,8 @@ export class Collector extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a Promise resolved when Collector ends or a timeout occurs */
|
/** Returns a Promise resolved when Collector ends or a timeout occurs */
|
||||||
// eslint-disable-next-line
|
async wait(timeout?: number): Promise<Collector> {
|
||||||
async wait(timeout: number = this.timeout ?? 0): Promise<Collector> {
|
if (timeout === undefined) timeout = this.timeout ?? 0
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
if (!timeout)
|
if (!timeout)
|
||||||
|
@ -147,14 +154,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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
async getShardCount(): Promise<number> {
|
||||||
throw new Error('Token should be provided when constructing ShardManager')
|
let shardCount: number
|
||||||
if (this.__client.intents === undefined || options.intents === undefined)
|
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
|
||||||
throw new Error(
|
else {
|
||||||
'Intents should be provided when constructing ShardManager'
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 shardCount = await this.getShardCount()
|
||||||
|
|
||||||
|
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.token = this.__client.token ?? options.token
|
return gw.waitFor(GatewayEvents.Ready, () => true).then(() => this)
|
||||||
this.intents = this.__client.intents ?? options.intents
|
|
||||||
this.shardCount = options.shards
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// static async init(): Promise<ShardManager> {}
|
async start(): Promise<ShardManager> {
|
||||||
|
const shardCount = await this.getShardCount()
|
||||||
// async start(): Promise<ShardManager> {
|
for (let i = 0; i <= shardCount; i++) {
|
||||||
// const info = ((await this.rest.get(
|
await this.launch(i)
|
||||||
// GATEWAY_BOT()
|
}
|
||||||
// )) as unknown) as GatewayBotPayload
|
return this
|
||||||
|
}
|
||||||
// const totalShards = this.__shardCount ?? info.shards
|
|
||||||
|
|
||||||
// return this
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
GuildFeatures,
|
GuildFeatures,
|
||||||
GuildIntegrationPayload,
|
GuildIntegrationPayload,
|
||||||
GuildPayload,
|
GuildPayload,
|
||||||
|
GuildWidgetPayload,
|
||||||
IntegrationAccountPayload,
|
IntegrationAccountPayload,
|
||||||
IntegrationExpireBehavior,
|
IntegrationExpireBehavior,
|
||||||
Verification,
|
Verification,
|
||||||
|
@ -38,6 +39,8 @@ import {
|
||||||
import { GuildVoiceStatesManager } from '../managers/guildVoiceStates.ts'
|
import { GuildVoiceStatesManager } from '../managers/guildVoiceStates.ts'
|
||||||
import { RequestMembersOptions } from '../gateway/index.ts'
|
import { RequestMembersOptions } from '../gateway/index.ts'
|
||||||
import { GuildPresencesManager } from '../managers/presences.ts'
|
import { GuildPresencesManager } from '../managers/presences.ts'
|
||||||
|
import { TemplatePayload } from '../types/template.ts'
|
||||||
|
import { Template } from './template.ts'
|
||||||
|
|
||||||
export class GuildBan extends Base {
|
export class GuildBan extends Base {
|
||||||
guild: Guild
|
guild: Guild
|
||||||
|
@ -328,18 +331,137 @@ export class Guild extends Base {
|
||||||
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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. */
|
/** Gets a preview of the guild. Returns GuildPreview. */
|
||||||
async preview(): Promise<GuildPreview> {
|
async preview(): Promise<GuildPreview> {
|
||||||
return this.client.guilds.preview(this.id)
|
return this.client.guilds.preview(this.id)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -162,6 +162,15 @@ export interface GuildBanPayload {
|
||||||
user: UserPayload
|
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 =
|
export type GuildChannelPayloads =
|
||||||
| GuildTextChannelPayload
|
| GuildTextChannelPayload
|
||||||
| GuildVoiceChannelPayload
|
| GuildVoiceChannelPayload
|
||||||
|
|
|
@ -42,3 +42,19 @@ export interface VoiceStatePayload {
|
||||||
self_video: boolean
|
self_video: boolean
|
||||||
suppress: 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…
Reference in a new issue