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:
DjDeveloper 2021-01-21 19:22:20 +05:30 committed by GitHub
commit b2a93769ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 560 additions and 189 deletions

View File

@ -1,4 +1,4 @@
export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts'
export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts'
export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'

2
mod.ts
View File

@ -1,6 +1,7 @@
export { GatewayIntents } from './src/types/gateway.ts'
export { Base } from './src/structures/base.ts'
export { Gateway } from './src/gateway/index.ts'
export type { GatewayTypedEvents } from './src/gateway/index.ts'
export type { ClientEvents } from './src/gateway/handlers/index.ts'
export * from './src/models/client.ts'
export * from './src/models/slashClient.ts'
@ -124,3 +125,4 @@ export type { UserPayload } from './src/types/user.ts'
export { UserFlags } from './src/types/userFlags.ts'
export type { VoiceStatePayload } from './src/types/voice.ts'
export type { WebhookPayload } from './src/types/webhook.ts'
export * from './src/models/collectors.ts'

View File

@ -1,5 +1,9 @@
import { GatewayEventHandler } from '../index.ts'
import { GatewayEvents, TypingStartGuildData } from '../../types/gateway.ts'
import {
GatewayEvents,
MessageDeletePayload,
TypingStartGuildData
} from '../../types/gateway.ts'
import { channelCreate } from './channelCreate.ts'
import { channelDelete } from './channelDelete.ts'
import { channelUpdate } from './channelUpdate.ts'
@ -55,6 +59,10 @@ import {
} from '../../utils/getChannelByType.ts'
import { interactionCreate } from './interactionCreate.ts'
import { Interaction } from '../../structures/slash.ts'
import { CommandContext } from '../../models/command.ts'
import { RequestMethods } from '../../models/rest.ts'
import { PartialInvitePayload } from '../../types/invite.ts'
import { GuildChannels } from '../../types/guild.ts'
export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
@ -105,7 +113,8 @@ export interface VoiceServerUpdateData {
guild: Guild
}
export interface ClientEvents {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ClientEvents = {
/** When Client has successfully connected to Discord */
ready: []
/** When a successful reconnect has been made */
@ -355,4 +364,40 @@ export interface ClientEvents {
* @param payload Payload JSON of the event
*/
raw: [evt: string, payload: any]
/**
* An uncached Message was deleted.
* @param payload Message Delete Payload
*/
messageDeleteUncached: [payload: MessageDeletePayload]
guildMembersChunk: [
guild: Guild,
info: {
chunkIndex: number
chunkCount: number
members: string[]
presences: string[] | undefined
}
]
guildMembersChunked: [guild: Guild, chunks: number]
rateLimit: [data: { method: RequestMethods; url: string; body: any }]
inviteDeleteUncached: [invite: PartialInvitePayload]
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
userUpdateUncached: [user: User]
webhooksUpdateUncached: [guild: Guild, channelID: string]
guildRoleUpdateUncached: [role: Role]
guildMemberUpdateUncached: [member: Member]
guildMemberRemoveUncached: [member: Member]
channelUpdateUncached: [channel: GuildChannels]
commandOwnerOnly: [ctx: CommandContext]
commandGuildOnly: [ctx: CommandContext]
commandDmOnly: [ctx: CommandContext]
commandNSFW: [ctx: CommandContext]
commandBotMissingPermissions: [ctx: CommandContext, missing: string[]]
commandUserMissingPermissions: [ctx: CommandContext, missing: string[]]
commandMissingArgs: [ctx: CommandContext]
commandUsed: [ctx: CommandContext]
commandError: [ctx: CommandContext, err: Error]
}

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -89,4 +89,20 @@ export class GuildChannelsManager extends BaseChildManager<
const channel = await this.get(res.id)
return (channel as unknown) as GuildChannels
}
/** Modify the positions of a set of channel positions for the guild. */
async editPositions(
...positions: Array<{ id: string | GuildChannels; position: number | null }>
): Promise<GuildChannelsManager> {
if (positions.length === 0)
throw new Error('No channel positions to change specified')
await this.client.rest.api.guilds[this.guild.id].channels.patch(
positions.map((e) => ({
id: typeof e.id === 'string' ? e.id : e.id.id,
position: e.position ?? null
}))
)
return this
}
}

View File

@ -1,5 +1,7 @@
import { fetchAuto } from '../../deps.ts'
import { Client } from '../models/client.ts'
import { Guild } from '../structures/guild.ts'
import { Template } from '../structures/template.ts'
import { Role } from '../structures/role.ts'
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
import {
@ -16,7 +18,6 @@ import {
} from '../types/guild.ts'
import { BaseManager } from './base.ts'
import { MembersManager } from './members.ts'
import { fetchAuto } from '../../deps.ts'
import { Emoji } from '../structures/emoji.ts'
export class GuildManager extends BaseManager<GuildPayload, Guild> {
@ -47,6 +48,19 @@ export class GuildManager extends BaseManager<GuildPayload, Guild> {
})
}
/** Create a new guild based on a template. */
async createFromTemplate(
template: Template | string,
name: string,
icon?: string
): Promise<Guild> {
if (icon?.startsWith('http') === true) icon = await fetchAuto(icon)
const guild = await this.client.rest.api.guilds.templates[
typeof template === 'object' ? template.code : template
].post({ name, icon })
return new Guild(this.client, guild)
}
/**
* Creates a guild. Returns Guild. Fires guildCreate event.
* @param options Options for creating a guild

View File

@ -101,4 +101,20 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
return new Role(this.client, resp, this.guild)
}
/** Modify the positions of a set of role positions for the guild. */
async editPositions(
...positions: Array<{ id: string | Role; position: number | null }>
): Promise<RolesManager> {
if (positions.length === 0)
throw new Error('No role positions to change specified')
await this.client.rest.api.guilds[this.guild.id].roles.patch(
positions.map((e) => ({
id: typeof e.id === 'string' ? e.id : e.id.id,
position: e.position ?? null
}))
)
return this
}
}

View File

@ -3,7 +3,6 @@ import { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts'
import { RESTManager, RESTOptions, TokenType } from './rest.ts'
import { EventEmitter } from '../../deps.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UsersManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts'
@ -21,6 +20,11 @@ import { Invite } from '../structures/invite.ts'
import { INVITE } from '../types/endpoint.ts'
import { ClientEvents } from '../gateway/handlers/index.ts'
import type { Collector } from './collectors.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
import { VoiceRegion } from '../types/voice.ts'
import { fetchAuto } from '../../deps.ts'
import { DMChannel } from '../structures/dmChannel.ts'
import { Template } from '../structures/template.ts'
/** OS related properties sent with Gateway Identify */
export interface ClientProperties {
@ -59,40 +63,20 @@ export interface ClientOptions {
disableEnvToken?: boolean
/** Override REST Options */
restOptions?: RESTOptions
}
export declare interface Client {
on<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
on(event: string | symbol, listener: (...args: any[]) => void): this
once<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
once(event: string | symbol, listener: (...args: any[]) => void): this
emit<K extends keyof ClientEvents>(
event: K,
...args: ClientEvents[K]
): boolean
emit(event: string | symbol, ...args: any[]): boolean
off<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
off(event: string | symbol, listener: (...args: any[]) => void): this
/** Whether to fetch Gateway info or not */
fetchGatewayInfo?: boolean
/** ADVANCED: Shard ID to launch on */
shard?: number
/** Shard count. Set to 'auto' for automatic sharding */
shardCount?: number | 'auto'
}
/**
* Discord Client.
*/
export class Client extends EventEmitter {
export class Client extends HarmonyEventEmitter<ClientEvents> {
/** Gateway object */
gateway?: Gateway
gateway: Gateway
/** REST Manager - used to make all requests */
rest: RESTManager
/** User which Client logs in to, undefined until logs in */
@ -117,12 +101,21 @@ export class Client extends EventEmitter {
clientProperties: ClientProperties
/** Slash-Commands Management client */
slash: SlashClient
/** Whether to fetch Gateway info or not */
fetchGatewayInfo: boolean = true
/** Users Manager, containing all Users cached */
users: UsersManager = new UsersManager(this)
/** Guilds Manager, providing cache & API interface to Guilds */
guilds: GuildManager = new GuildManager(this)
/** Channels Manager, providing cache interface to Channels */
channels: ChannelsManager = new ChannelsManager(this)
/** Channels Manager, providing cache interface to Channels */
emojis: EmojisManager = new EmojisManager(this)
/** Last READY timestamp */
upSince?: Date
/** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence()
_decoratedEvents?: {
@ -141,10 +134,23 @@ export class Client extends EventEmitter {
/** Shard on which this Client is */
shard: number = 0
/** Shard Count */
shardCount: number | 'auto' = 1
/** Shard Manager of this Client if Sharded */
shardManager?: ShardManager
shards?: ShardManager
/** Collectors set */
collectors: Set<Collector> = new Set()
/** Since when is Client online (ready). */
get uptime(): number {
if (this.upSince === undefined) return 0
else {
const dif = Date.now() - this.upSince.getTime()
if (dif < 0) return dif
else return dif
}
}
constructor(options: ClientOptions = {}) {
super()
this._id = options.id
@ -169,7 +175,7 @@ export class Client extends EventEmitter {
Object.keys(this._decoratedEvents).length !== 0
) {
Object.entries(this._decoratedEvents).forEach((entry) => {
this.on(entry[0], entry[1])
this.on(entry[0] as keyof ClientEvents, entry[1])
})
this._decoratedEvents = undefined
}
@ -183,12 +189,17 @@ export class Client extends EventEmitter {
}
: options.clientProperties
if (options.shard !== undefined) this.shard = options.shard
if (options.shardCount !== undefined) this.shardCount = options.shardCount
this.slash = new SlashClient({
id: () => this.getEstimatedID(),
client: this,
enabled: options.enableSlash
})
this.fetchGatewayInfo = options.fetchGatewayInfo ?? false
if (this.token === undefined) {
try {
const token = Deno.env.get('DISCORD_TOKEN')
@ -209,6 +220,7 @@ export class Client extends EventEmitter {
if (options.restOptions !== undefined)
Object.assign(restOptions, options.restOptions)
this.rest = new RESTManager(restOptions)
this.gateway = new Gateway(this)
}
/**
@ -232,6 +244,7 @@ export class Client extends EventEmitter {
/** Emits debug event */
debug(tag: string, msg: string): void {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.emit('debug', `[${tag}] ${msg}`)
}
@ -271,7 +284,7 @@ export class Client extends EventEmitter {
* @param token Your token. This is required if not given in ClientOptions.
* @param intents Gateway intents in array. This is required if not given in ClientOptions.
*/
connect(token?: string, intents?: GatewayIntents[]): void {
async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> {
if (token === undefined && this.token !== undefined) token = this.token
else if (this.token === undefined && token !== undefined) {
this.token = token
@ -288,7 +301,30 @@ export class Client extends EventEmitter {
} else throw new Error('No Gateway Intents were provided')
this.rest.token = token
this.gateway = new Gateway(this, token, intents)
this.gateway.token = token
this.gateway.intents = intents
this.gateway.initWebsocket()
return this.waitFor('ready', () => true).then(() => this)
}
/** Destroy the Gateway connection */
async destroy(): Promise<Client> {
this.gateway.initialized = false
this.gateway.sequenceID = undefined
this.gateway.sessionID = undefined
await this.gateway.cache.delete('seq')
await this.gateway.cache.delete('session_id')
this.gateway.close()
this.user = undefined
this.upSince = undefined
return this
}
/** Attempt to Close current Gateway connection and Resume */
async reconnect(): Promise<Client> {
this.gateway.close()
this.gateway.initWebsocket()
return this.waitFor('ready', () => true).then(() => this)
}
/** Add a new Collector */
@ -309,7 +345,7 @@ export class Client extends EventEmitter {
}
}
emit(event: keyof ClientEvents, ...args: any[]): boolean {
async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> {
const collectors: Collector[] = []
for (const collector of this.collectors.values()) {
if (collector.event === event) collectors.push(collector)
@ -317,32 +353,62 @@ export class Client extends EventEmitter {
if (collectors.length !== 0) {
this.collectors.forEach((collector) => collector._fire(...args))
}
// TODO(DjDeveloperr): Fix this ts-ignore
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore
return super.emit(event, ...args)
}
/** Wait for an Event (optionally satisfying an event) to occur */
async waitFor<K extends keyof ClientEvents>(
event: K,
checkFunction: (...args: ClientEvents[K]) => boolean,
timeout?: number
): Promise<ClientEvents[K] | []> {
return await new Promise((resolve) => {
let timeoutID: number | undefined
if (timeout !== undefined) {
timeoutID = setTimeout(() => {
this.off(event, eventFunc)
resolve([])
}, timeout)
}
const eventFunc = (...args: ClientEvents[K]): void => {
if (checkFunction(...args)) {
resolve(args)
this.off(event, eventFunc)
if (timeoutID !== undefined) clearTimeout(timeoutID)
}
}
this.on(event, eventFunc)
/** Returns an array of voice region objects that can be used when creating servers. */
async fetchVoiceRegions(): Promise<VoiceRegion[]> {
return this.rest.api.voice.regions.get()
}
/** Modify current (Client) User. */
async editUser(data: {
username?: string
avatar?: string
}): Promise<Client> {
if (data.username === undefined && data.avatar === undefined)
throw new Error(
'Either username or avatar or both must be specified to edit'
)
if (data.avatar?.startsWith('http') === true) {
data.avatar = await fetchAuto(data.avatar)
}
await this.rest.api.users['@me'].patch({
username: data.username,
avatar: data.avatar
})
return this
}
/** Change Username of the Client User */
async setUsername(username: string): Promise<Client> {
return await this.editUser({ username })
}
/** Change Avatar of the Client User */
async setAvatar(avatar: string): Promise<Client> {
return await this.editUser({ avatar })
}
/** Create a DM Channel with a User */
async createDM(user: User | string): Promise<DMChannel> {
const id = typeof user === 'object' ? user.id : user
const dmPayload = await this.rest.api.users['@me'].channels.post({
recipient_id: id
})
await this.channels.set(dmPayload.id, dmPayload)
return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel
}
/** Returns a template object for the given code. */
async fetchTemplate(code: string): Promise<Template> {
const payload = await this.rest.api.guilds.templates[code].get()
return new Template(this, payload)
}
}

View File

@ -1,6 +1,6 @@
import { Collection } from '../utils/collection.ts'
import { EventEmitter } from '../../deps.ts'
import type { Client } from './client.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
@ -19,7 +19,14 @@ export interface CollectorOptions {
timeout?: number
}
export class Collector extends EventEmitter {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type CollectorEvents = {
start: []
end: []
collect: any
}
export class Collector extends HarmonyEventEmitter<CollectorEvents> {
client?: Client
private _started: boolean = false
event: string
@ -135,8 +142,8 @@ export class Collector extends EventEmitter {
}
/** Returns a Promise resolved when Collector ends or a timeout occurs */
// eslint-disable-next-line
async wait(timeout: number = this.timeout ?? 0): Promise<Collector> {
async wait(timeout?: number): Promise<Collector> {
if (timeout === undefined) timeout = this.timeout ?? 0
return await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!timeout)
@ -147,14 +154,14 @@ export class Collector extends EventEmitter {
let done = false
const onend = (): void => {
done = true
this.removeListener('end', onend)
this.off('end', onend)
resolve(this)
}
this.on('end', onend)
setTimeout(() => {
if (!done) {
this.removeListener('end', onend)
this.off('end', onend)
reject(new Error('Timeout'))
}
}, timeout)

View File

@ -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)
}
}
}

View File

@ -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]
}

View File

@ -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
}
}

View File

@ -4,6 +4,7 @@ import {
GuildFeatures,
GuildIntegrationPayload,
GuildPayload,
GuildWidgetPayload,
IntegrationAccountPayload,
IntegrationExpireBehavior,
Verification,
@ -38,6 +39,8 @@ import {
import { GuildVoiceStatesManager } from '../managers/guildVoiceStates.ts'
import { RequestMembersOptions } from '../gateway/index.ts'
import { GuildPresencesManager } from '../managers/presences.ts'
import { TemplatePayload } from '../types/template.ts'
import { Template } from './template.ts'
export class GuildBan extends Base {
guild: Guild
@ -328,18 +331,137 @@ export class Guild extends Base {
if (!this.unavailable) resolve(this)
const listener = (guild: Guild): void => {
if (guild.id === this.id) {
this.client.removeListener('guildLoaded', listener)
this.client.off('guildLoaded', listener)
resolve(this)
}
}
this.client.on('guildLoaded', listener)
setTimeout(() => {
this.client.removeListener('guildLoaded', listener)
this.client.off('guildLoaded', listener)
reject(Error("Timeout. Guild didn't arrive in time."))
}, timeout)
})
}
/** Attach an integration object from the current user to the guild. */
async createIntegration(id: string, type: string): Promise<Guild> {
await this.client.rest.api.guilds[this.id].integrations.post({ id, type })
return this
}
/** Modify the behavior and settings of an integration object for the guild. */
async editIntegration(
id: string,
data: {
expireBehavior?: number | null
expireGracePeriod?: number | null
enableEmoticons?: boolean | null
}
): Promise<Guild> {
await this.client.rest.api.guilds[this.id].integrations[id].patch({
expire_behaviour: data.expireBehavior,
expire_grace_period: data.expireGracePeriod,
enable_emoticons: data.enableEmoticons
})
return this
}
/** Delete the attached integration object for the guild. Deletes any associated webhooks and kicks the associated bot if there is one. */
async deleteIntegration(id: string): Promise<Guild> {
await this.client.rest.api.guilds[this.id].integrations[id].delete()
return this
}
/** Sync an integration. */
async syncIntegration(id: string): Promise<Guild> {
await this.client.rest.api.guilds[this.id].integrations[id].sync.post()
return this
}
/** Returns the widget for the guild. */
async getWidget(): Promise<GuildWidgetPayload> {
return this.client.rest.api.guilds[this.id]['widget.json'].get()
}
/** Modify a guild widget object for the guild. */
async editWidget(data: {
enabled?: boolean
channel?: string | GuildChannels
}): Promise<Guild> {
await this.client.rest.api.guilds[this.id].widget.patch({
enabled: data.enabled,
channel_id:
typeof data.channel === 'object' ? data.channel.id : data.channel
})
return this
}
/** Returns a partial invite object for guilds with that feature enabled. */
async getVanity(): Promise<{ code: string | null; uses: number }> {
return this.client.rest.api.guilds[this.id]['vanity-url'].get()
}
/** Returns a PNG (URL) image widget for the guild. */
getWidgetImageURL(
style?: 'shield' | 'banner1' | 'banner2' | 'banner3' | 'banner4'
): string {
return `https://discord.com/api/v${this.client.rest.version ?? 8}/guilds/${
this.id
}/widget.png${style !== undefined ? `?style=${style}` : ''}`
}
/** Leave a Guild. */
async leave(): Promise<Client> {
await this.client.rest.api.users['@me'].guilds[this.id].delete()
return this.client
}
/** Returns an array of template objects. */
async getTemplates(): Promise<Template[]> {
return this.client.rest.api.guilds[this.id].templates
.get()
.then((temps: TemplatePayload[]) =>
temps.map((temp) => new Template(this.client, temp))
)
}
/** Creates a template for the guild. */
async createTemplate(
name: string,
description?: string | null
): Promise<Template> {
const payload = await this.client.rest.api.guilds[this.id].templates.post({
name,
description
})
return new Template(this.client, payload)
}
/** Syncs the template to the guild's current state. */
async syncTemplate(code: string): Promise<Template> {
const payload = await this.client.rest.api.guilds[this.id].templates[
code
].sync.put()
return new Template(this.client, payload)
}
/** Modifies the template's metadata. */
async editTemplate(
code: string,
data: { name?: string; description?: string }
): Promise<Template> {
const payload = await this.client.rest.api.guilds[this.id].templates[
code
].patch({ name: data.name, description: data.description })
return new Template(this.client, payload)
}
/** Deletes the template. Requires the MANAGE_GUILD permission. */
async deleteTemplate(code: string): Promise<Guild> {
await this.client.rest.api.guilds[this.id].templates[code].delete()
return this
}
/** Gets a preview of the guild. Returns GuildPreview. */
async preview(): Promise<GuildPreview> {
return this.client.guilds.preview(this.id)

View File

@ -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"

View File

@ -162,6 +162,15 @@ export interface GuildBanPayload {
user: UserPayload
}
export interface GuildWidgetPayload {
id: string
name: string
instant_invite: string
channels: Array<{ id: string; name: string; position: number }>
members: MemberPayload[]
presence_count: number
}
export type GuildChannelPayloads =
| GuildTextChannelPayload
| GuildVoiceChannelPayload

View File

@ -42,3 +42,19 @@ export interface VoiceStatePayload {
self_video: boolean
suppress: boolean
}
/** Voice Region Structure */
export interface VoiceRegion {
/** Unique ID for the region */
id: string
/** Name of the region */
name: string
/** True if this is a vip-only server */
vip: boolean
/** True for a single server that is closest to the current user's client */
optimal: boolean
/** Whether this is a deprecated voice region (avoid switching to these) */
deprecated: boolean
/** Whether this is a custom voice region (used for events/etc) */
custom: boolean
}

30
src/utils/events.ts Normal file
View File

@ -0,0 +1,30 @@
import { EventEmitter } from '../../deps.ts'
export class HarmonyEventEmitter<
T extends Record<string, unknown[]>
> extends EventEmitter<T> {
/** Wait for an Event to fire with given condition. */
async waitFor<K extends keyof T>(
event: K,
checkFunction: (...args: T[K]) => boolean = () => true,
timeout?: number
): Promise<T[K] | []> {
return await new Promise((resolve) => {
let timeoutID: number | undefined
if (timeout !== undefined) {
timeoutID = setTimeout(() => {
this.off(event, eventFunc)
resolve([])
}, timeout)
}
const eventFunc = (...args: T[K]): void => {
if (checkFunction(...args)) {
resolve(args)
this.off(event, eventFunc)
if (timeoutID !== undefined) clearTimeout(timeoutID)
}
}
this.on(event, eventFunc)
})
}
}