From cac3c5c9e36312b0edc4ce8da2d1a0650bb0ad11 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sun, 20 Dec 2020 15:15:49 +0530 Subject: [PATCH] chore: make slashclient standalone and restmanager accept options --- src/gateway/index.ts | 20 ++++++-- src/models/client.ts | 29 ++++++++++-- src/models/rest.ts | 19 ++++++-- src/models/slashClient.ts | 97 ++++++++++++++++++++++++++++----------- src/test/music.ts | 90 ++++++++++++++++++------------------ src/test/slash-only.ts | 6 +++ 6 files changed, 177 insertions(+), 84 deletions(-) create mode 100644 src/test/slash-only.ts diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 4f8dacb..a8349a4 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -18,6 +18,7 @@ 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 EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' export interface RequestMembersOptions { limit?: number @@ -34,11 +35,11 @@ export interface VoiceStateOptions { export const RECONNECT_REASON = 'harmony-reconnect' /** - * Handles Discord gateway connection. + * Handles Discord Gateway connection. * * You should not use this and rather use Client class. */ -class Gateway { +export class Gateway extends EventEmitter { websocket: WebSocket token: string intents: GatewayIntents[] @@ -55,6 +56,7 @@ class Gateway { private timedIdentify: number | null = null constructor(client: Client, token: string, intents: GatewayIntents[]) { + super() this.token = token this.intents = intents this.client = client @@ -74,6 +76,7 @@ class Gateway { private onopen(): void { this.connected = true this.debug('Connected to Gateway!') + this.emit('connect') } private async onmessage(event: MessageEvent): Promise { @@ -112,6 +115,7 @@ class Gateway { case GatewayOpcodes.HEARTBEAT_ACK: this.heartbeatServerResponded = true this.client.ping = Date.now() - this.lastPingTimestamp + this.emit('ping', this.client.ping) this.debug( `Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms` ) @@ -142,6 +146,7 @@ class Gateway { await this.cache.set('seq', s) } if (t !== null && t !== undefined) { + this.emit(t, d) this.client.emit('raw', t, d) const handler = gatewayHandlers[t] @@ -158,9 +163,11 @@ class Gateway { this.sequenceID = d.seq await this.cache.set('seq', d.seq) await this.cache.set('session_id', this.sessionID) + this.emit('resume') break } case GatewayOpcodes.RECONNECT: { + this.emit('reconnectRequired') // eslint-disable-next-line @typescript-eslint/no-floating-promises this.reconnect() break @@ -172,6 +179,7 @@ class Gateway { private async onclose(event: CloseEvent): Promise { if (event.reason === RECONNECT_REASON) return + this.emit('close', event.code, event.reason) this.debug(`Connection Closed with code: ${event.code}`) if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { @@ -223,7 +231,7 @@ class Gateway { private onerror(event: Event | ErrorEvent): void { const eventError = event as ErrorEvent - console.log(eventError) + this.emit('error', eventError) } private async sendIdentify(forceNewSession?: boolean): Promise { @@ -266,6 +274,7 @@ class Gateway { } this.debug('Sending Identify payload...') + this.emit('sentIdentify') this.send({ op: GatewayOpcodes.IDENTIFY, d: payload @@ -291,6 +300,7 @@ class Gateway { seq: this.sequenceID ?? null } } + this.emit('sentResume') this.debug('Sending Resume payload...') this.send(resumePayload) } @@ -341,6 +351,7 @@ class Gateway { } async reconnect(forceNew?: boolean): Promise { + this.emit('reconnecting') clearInterval(this.heartbeatIntervalID) if (forceNew === true) { await this.cache.delete('session_id') @@ -351,6 +362,7 @@ class Gateway { } initWebsocket(): void { + this.emit('init') this.debug('Initializing WebSocket...') this.websocket = new WebSocket( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -414,5 +426,3 @@ class Gateway { } export type GatewayEventHandler = (gateway: Gateway, d: any) => void - -export { Gateway } diff --git a/src/models/client.ts b/src/models/client.ts index a807fb9..07955d5 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -16,6 +16,7 @@ import { SlashClient } from './slashClient.ts' import { Interaction } from '../structures/slash.ts' import { SlashModule } from './slashModule.ts' import type { ShardManager } from './shard.ts' +import { Application } from '../structures/application.ts' /** OS related properties sent with Gateway Identify */ export interface ClientProperties { @@ -26,6 +27,8 @@ export interface ClientProperties { /** Some Client Options to modify behaviour */ export interface ClientOptions { + /** ID of the Client/Application to initialize Slash Client REST */ + id?: string /** Token of the Bot/User */ token?: string /** Gateway Intents */ @@ -100,6 +103,7 @@ export class Client extends EventEmitter { }> _decoratedSlashModules?: SlashModule[] + _id?: string private readonly _untypedOn = this.on @@ -120,6 +124,7 @@ export class Client extends EventEmitter { constructor(options: ClientOptions = {}) { super() + this._id = options.id this.token = options.token this.intents = options.intents this.forceNewSession = options.forceNewSession @@ -156,7 +161,9 @@ export class Client extends EventEmitter { } : options.clientProperties - this.slash = new SlashClient(this, { + this.slash = new SlashClient({ + id: () => this.getEstimatedID(), + client: this, enabled: options.enableSlash }) } @@ -185,8 +192,24 @@ export class Client extends EventEmitter { this.emit('debug', `[${tag}] ${msg}`) } - // TODO(DjDeveloperr): Implement this - // fetchApplication(): Promise + getEstimatedID(): string { + if (this.user !== undefined) return this.user.id + else if (this.token !== undefined) { + try { + return atob(this.token.split('.')[0]) + } catch (e) { + return this._id ?? 'unknown' + } + } else { + return this._id ?? 'unknown' + } + } + + /** Fetch Application of the Client */ + async fetchApplication(): Promise { + const app = await this.rest.api.oauth2.applications['@me'].get() + return new Application(this, app) + } /** * This function is used for connecting to discord. diff --git a/src/models/rest.ts b/src/models/rest.ts index ed39f59..0e80c39 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -1,5 +1,4 @@ import * as baseEndpoints from '../consts/urlsAndVersions.ts' -import { Client } from './client.ts' import { Collection } from '../utils/collection.ts' export type RequestMethods = @@ -94,8 +93,14 @@ export const builder = (rest: RESTManager, acum = '/'): APIMap => { return (proxy as unknown) as APIMap } +export interface RESTOptions { + token?: string + headers?: { [name: string]: string | undefined } + canary?: boolean +} + export class RESTManager { - client?: Client + client?: RESTOptions queues: { [key: string]: QueuedItem[] } = {} rateLimits = new Collection() globalRateLimit: boolean = false @@ -103,7 +108,7 @@ export class RESTManager { version: number = 8 api: APIMap - constructor(client?: Client) { + constructor(client?: RESTOptions) { this.client = client this.api = builder(this) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -206,10 +211,16 @@ export class RESTManager { form.append('file', body.file.blob, body.file.name) form.append('payload_json', JSON.stringify({ ...body, file: undefined })) body.file = form - } else if (body !== undefined && !['get', 'delete'].includes(method)) { + } else if ( + body !== undefined && + !['get', 'delete'].includes(method.toLowerCase()) + ) { headers['Content-Type'] = 'application/json' } + if (this.client?.headers !== undefined) + Object.assign(headers, this.client.headers) + const data: { [name: string]: any } = { headers, body: body?.file ?? JSON.stringify(body), diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 74a0ea3..5058660 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -14,10 +14,7 @@ import { } from '../types/slash.ts' import { Collection } from '../utils/collection.ts' import { Client } from './client.ts' - -export interface SlashOptions { - enabled?: boolean -} +import { RESTManager } from './rest.ts' export class SlashCommand { slash: SlashCommandsManager @@ -47,20 +44,22 @@ export class SlashCommand { } export class SlashCommandsManager { - client: Client slash: SlashClient - constructor(client: Client) { - this.client = client - this.slash = client.slash + get rest(): RESTManager { + return this.slash.rest + } + + constructor(client: SlashClient) { + this.slash = client } /** Get all Global Slash Commands */ async all(): Promise> { const col = new Collection() - const res = (await this.client.rest.get( - APPLICATION_COMMANDS(this.client.user?.id as string) + const res = (await this.rest.get( + APPLICATION_COMMANDS(this.slash.getID()) )) as SlashCommandPayload[] if (!Array.isArray(res)) return col @@ -78,9 +77,9 @@ export class SlashCommandsManager { ): Promise> { const col = new Collection() - const res = (await this.client.rest.get( + const res = (await this.rest.get( APPLICATION_GUILD_COMMANDS( - this.client.user?.id as string, + this.slash.getID(), typeof guild === 'string' ? guild : guild.id ) )) as SlashCommandPayload[] @@ -100,11 +99,11 @@ export class SlashCommandsManager { data: SlashCommandPartial, guild?: Guild | string ): Promise { - const payload = await this.client.rest.post( + const payload = await this.rest.post( guild === undefined - ? APPLICATION_COMMANDS(this.client.user?.id as string) + ? APPLICATION_COMMANDS(this.slash.getID()) : APPLICATION_GUILD_COMMANDS( - this.client.user?.id as string, + this.slash.getID(), typeof guild === 'string' ? guild : guild.id ), data @@ -123,11 +122,11 @@ export class SlashCommandsManager { data: SlashCommandPartial, guild?: Guild | string ): Promise { - await this.client.rest.patch( + await this.rest.patch( guild === undefined - ? APPLICATION_COMMAND(this.client.user?.id as string, id) + ? APPLICATION_COMMAND(this.slash.getID(), id) : APPLICATION_GUILD_COMMAND( - this.client.user?.id as string, + this.slash.getID(), typeof guild === 'string' ? guild : guild.id, id ), @@ -141,17 +140,31 @@ export class SlashCommandsManager { id: string, guild?: Guild | string ): Promise { - await this.client.rest.delete( + await this.rest.delete( guild === undefined - ? APPLICATION_COMMAND(this.client.user?.id as string, id) + ? APPLICATION_COMMAND(this.slash.getID(), id) : APPLICATION_GUILD_COMMAND( - this.client.user?.id as string, + this.slash.getID(), typeof guild === 'string' ? guild : guild.id, id ) ) return this } + + /** Get a Slash Command (global or Guild) */ + async get(id: string, guild?: Guild | string): Promise { + const data = await this.rest.get( + guild === undefined + ? APPLICATION_COMMAND(this.slash.getID(), id) + : APPLICATION_GUILD_COMMAND( + this.slash.getID(), + typeof guild === 'string' ? guild : guild.id, + id + ) + ) + return new SlashCommand(this, data) + } } export type SlashCommandHandlerCallback = (interaction: Interaction) => any @@ -163,31 +176,61 @@ export interface SlashCommandHandler { handler: SlashCommandHandlerCallback } +export interface SlashOptions { + id?: string | (() => string) + client?: Client + enabled?: boolean + token?: string + rest?: RESTManager +} + export class SlashClient { - client: Client + id: string | (() => string) + client?: Client + token?: string enabled: boolean = true commands: SlashCommandsManager handlers: SlashCommandHandler[] = [] + rest: RESTManager - constructor(client: Client, options?: SlashOptions) { - this.client = client - this.commands = new SlashCommandsManager(client) + constructor(options: SlashOptions) { + let id = options.id + if (options.token !== undefined) id = atob(options.token?.split('.')[0]) + if (id === undefined) + throw new Error('ID could not be found. Pass at least client or token') + this.id = id + this.client = options.client + this.token = options.token + this.commands = new SlashCommandsManager(this) if (options !== undefined) { this.enabled = options.enabled ?? true } - if (this.client._decoratedSlash !== undefined) { + if (this.client?._decoratedSlash !== undefined) { this.client._decoratedSlash.forEach((e) => { this.handlers.push(e) }) } - this.client.on('interactionCreate', (interaction) => + this.rest = + options.client === undefined + ? options.rest === undefined + ? new RESTManager({ + token: this.token + }) + : options.rest + : options.client.rest + + this.client?.on('interactionCreate', (interaction) => this._process(interaction) ) } + getID(): string { + return typeof this.id === 'string' ? this.id : this.id() + } + /** Adds a new Slash Command Handler */ handle(handler: SlashCommandHandler): SlashClient { this.handlers.push(handler) diff --git a/src/test/music.ts b/src/test/music.ts index fd84f3f..07f65b6 100644 --- a/src/test/music.ts +++ b/src/test/music.ts @@ -7,8 +7,7 @@ import { groupslash, CommandContext, Extension, - Collection, - SlashCommandOptionType + Collection } from '../../mod.ts' import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' import { @@ -85,51 +84,52 @@ class MyClient extends CommandClient { ready(): void { console.log(`Logged in as ${this.user?.tag}!`) this.manager.init(this.user?.id as string) + this.slash.commands.all().then(console.log) // this.rest.api.users['422957901716652033'].get().then(console.log) - client.slash.commands.create( - { - name: 'cmd', - description: 'Parent command!', - options: [ - { - name: 'sub-cmd-group', - type: SlashCommandOptionType.SUB_COMMAND_GROUP, - description: 'Sub Cmd Group', - options: [ - { - name: 'sub-cmd', - type: SlashCommandOptionType.SUB_COMMAND, - description: 'Sub Cmd' - } - ] - }, - { - name: 'sub-cmd-no-grp', - type: SlashCommandOptionType.SUB_COMMAND, - description: 'Sub Cmd' - }, - { - name: 'sub-cmd-grp-2', - type: SlashCommandOptionType.SUB_COMMAND_GROUP, - description: 'Sub Cmd Group 2', - options: [ - { - name: 'sub-cmd-1', - type: SlashCommandOptionType.SUB_COMMAND, - description: 'Sub Cmd 1' - }, - { - name: 'sub-cmd-2', - type: SlashCommandOptionType.SUB_COMMAND, - description: 'Sub Cmd 2' - } - ] - } - ] - }, - '783319033205751809' - ) + // client.slash.commands.create( + // { + // name: 'cmd', + // description: 'Parent command!', + // options: [ + // { + // name: 'sub-cmd-group', + // type: SlashCommandOptionType.SUB_COMMAND_GROUP, + // description: 'Sub Cmd Group', + // options: [ + // { + // name: 'sub-cmd', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd' + // } + // ] + // }, + // { + // name: 'sub-cmd-no-grp', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd' + // }, + // { + // name: 'sub-cmd-grp-2', + // type: SlashCommandOptionType.SUB_COMMAND_GROUP, + // description: 'Sub Cmd Group 2', + // options: [ + // { + // name: 'sub-cmd-1', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd 1' + // }, + // { + // name: 'sub-cmd-2', + // type: SlashCommandOptionType.SUB_COMMAND, + // description: 'Sub Cmd 2' + // } + // ] + // } + // ] + // }, + // '783319033205751809' + // ) // client.slash.commands.delete('788719077329207296', '783319033205751809') } } diff --git a/src/test/slash-only.ts b/src/test/slash-only.ts new file mode 100644 index 0000000..cd326bf --- /dev/null +++ b/src/test/slash-only.ts @@ -0,0 +1,6 @@ +import { SlashClient } from '../models/slashClient.ts' +import { TOKEN } from './config.ts' + +const slash = new SlashClient({ token: TOKEN }) + +slash.commands.all().then(console.log)