Merge pull request #66 from DjDeveloperr/slash

SlashClient & RESTManager can run alone and added APIMap
This commit is contained in:
Aki 2020-12-20 21:01:39 +09:00 committed by GitHub
commit 3695b81de6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 192 additions and 43 deletions

View file

@ -18,6 +18,7 @@ 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 EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
export interface RequestMembersOptions { export interface RequestMembersOptions {
limit?: number limit?: number
@ -34,11 +35,11 @@ export interface VoiceStateOptions {
export const RECONNECT_REASON = 'harmony-reconnect' export const RECONNECT_REASON = 'harmony-reconnect'
/** /**
* 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.
*/ */
class Gateway { export class Gateway extends EventEmitter {
websocket: WebSocket websocket: WebSocket
token: string token: string
intents: GatewayIntents[] intents: GatewayIntents[]
@ -55,6 +56,7 @@ class Gateway {
private timedIdentify: number | null = null private timedIdentify: number | null = null
constructor(client: Client, token: string, intents: GatewayIntents[]) { constructor(client: Client, token: string, intents: GatewayIntents[]) {
super()
this.token = token this.token = token
this.intents = intents this.intents = intents
this.client = client this.client = client
@ -74,6 +76,7 @@ class Gateway {
private onopen(): void { private onopen(): void {
this.connected = true this.connected = true
this.debug('Connected to Gateway!') this.debug('Connected to Gateway!')
this.emit('connect')
} }
private async onmessage(event: MessageEvent): Promise<void> { private async onmessage(event: MessageEvent): Promise<void> {
@ -112,6 +115,7 @@ class Gateway {
case GatewayOpcodes.HEARTBEAT_ACK: case GatewayOpcodes.HEARTBEAT_ACK:
this.heartbeatServerResponded = true this.heartbeatServerResponded = true
this.client.ping = Date.now() - this.lastPingTimestamp this.client.ping = Date.now() - this.lastPingTimestamp
this.emit('ping', this.client.ping)
this.debug( this.debug(
`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms` `Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`
) )
@ -142,6 +146,7 @@ class Gateway {
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.client.emit('raw', t, d) this.client.emit('raw', t, d)
const handler = gatewayHandlers[t] const handler = gatewayHandlers[t]
@ -158,9 +163,11 @@ class Gateway {
this.sequenceID = d.seq this.sequenceID = d.seq
await this.cache.set('seq', d.seq) await this.cache.set('seq', d.seq)
await this.cache.set('session_id', this.sessionID) await this.cache.set('session_id', this.sessionID)
this.emit('resume')
break break
} }
case GatewayOpcodes.RECONNECT: { case GatewayOpcodes.RECONNECT: {
this.emit('reconnectRequired')
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect() this.reconnect()
break break
@ -172,6 +179,7 @@ class Gateway {
private async onclose(event: CloseEvent): Promise<void> { private async onclose(event: CloseEvent): Promise<void> {
if (event.reason === RECONNECT_REASON) return if (event.reason === RECONNECT_REASON) return
this.emit('close', event.code, event.reason)
this.debug(`Connection Closed with code: ${event.code}`) this.debug(`Connection Closed with code: ${event.code}`)
if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) {
@ -223,7 +231,7 @@ class Gateway {
private onerror(event: Event | ErrorEvent): void { private onerror(event: Event | ErrorEvent): void {
const eventError = event as ErrorEvent const eventError = event as ErrorEvent
console.log(eventError) this.emit('error', eventError)
} }
private async sendIdentify(forceNewSession?: boolean): Promise<void> { private async sendIdentify(forceNewSession?: boolean): Promise<void> {
@ -266,6 +274,7 @@ class Gateway {
} }
this.debug('Sending Identify payload...') this.debug('Sending Identify payload...')
this.emit('sentIdentify')
this.send({ this.send({
op: GatewayOpcodes.IDENTIFY, op: GatewayOpcodes.IDENTIFY,
d: payload d: payload
@ -291,6 +300,7 @@ class Gateway {
seq: this.sequenceID ?? null seq: this.sequenceID ?? null
} }
} }
this.emit('sentResume')
this.debug('Sending Resume payload...') this.debug('Sending Resume payload...')
this.send(resumePayload) this.send(resumePayload)
} }
@ -341,6 +351,7 @@ class Gateway {
} }
async reconnect(forceNew?: boolean): Promise<void> { async reconnect(forceNew?: boolean): Promise<void> {
this.emit('reconnecting')
clearInterval(this.heartbeatIntervalID) clearInterval(this.heartbeatIntervalID)
if (forceNew === true) { if (forceNew === true) {
await this.cache.delete('session_id') await this.cache.delete('session_id')
@ -351,6 +362,7 @@ class Gateway {
} }
initWebsocket(): void { initWebsocket(): void {
this.emit('init')
this.debug('Initializing WebSocket...') this.debug('Initializing WebSocket...')
this.websocket = new WebSocket( this.websocket = new WebSocket(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
@ -414,5 +426,3 @@ class Gateway {
} }
export type GatewayEventHandler = (gateway: Gateway, d: any) => void export type GatewayEventHandler = (gateway: Gateway, d: any) => void
export { Gateway }

View file

@ -16,6 +16,7 @@ import { SlashClient } from './slashClient.ts'
import { Interaction } from '../structures/slash.ts' import { Interaction } from '../structures/slash.ts'
import { SlashModule } from './slashModule.ts' import { SlashModule } from './slashModule.ts'
import type { ShardManager } from './shard.ts' import type { ShardManager } from './shard.ts'
import { Application } from '../structures/application.ts'
/** OS related properties sent with Gateway Identify */ /** OS related properties sent with Gateway Identify */
export interface ClientProperties { export interface ClientProperties {
@ -26,6 +27,8 @@ export interface ClientProperties {
/** Some Client Options to modify behaviour */ /** Some Client Options to modify behaviour */
export interface ClientOptions { export interface ClientOptions {
/** ID of the Client/Application to initialize Slash Client REST */
id?: string
/** Token of the Bot/User */ /** Token of the Bot/User */
token?: string token?: string
/** Gateway Intents */ /** Gateway Intents */
@ -100,6 +103,7 @@ export class Client extends EventEmitter {
}> }>
_decoratedSlashModules?: SlashModule[] _decoratedSlashModules?: SlashModule[]
_id?: string
private readonly _untypedOn = this.on private readonly _untypedOn = this.on
@ -120,6 +124,7 @@ export class Client extends EventEmitter {
constructor(options: ClientOptions = {}) { constructor(options: ClientOptions = {}) {
super() super()
this._id = options.id
this.token = options.token this.token = options.token
this.intents = options.intents this.intents = options.intents
this.forceNewSession = options.forceNewSession this.forceNewSession = options.forceNewSession
@ -156,7 +161,9 @@ export class Client extends EventEmitter {
} }
: options.clientProperties : options.clientProperties
this.slash = new SlashClient(this, { this.slash = new SlashClient({
id: () => this.getEstimatedID(),
client: this,
enabled: options.enableSlash enabled: options.enableSlash
}) })
} }
@ -185,8 +192,24 @@ export class Client extends EventEmitter {
this.emit('debug', `[${tag}] ${msg}`) this.emit('debug', `[${tag}] ${msg}`)
} }
// TODO(DjDeveloperr): Implement this getEstimatedID(): string {
// fetchApplication(): Promise<Application> 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<Application> {
const app = await this.rest.api.oauth2.applications['@me'].get()
return new Application(this, app)
}
/** /**
* This function is used for connecting to discord. * This function is used for connecting to discord.

View file

@ -1,5 +1,4 @@
import * as baseEndpoints from '../consts/urlsAndVersions.ts' import * as baseEndpoints from '../consts/urlsAndVersions.ts'
import { Client } from './client.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
export type RequestMethods = export type RequestMethods =
@ -51,15 +50,67 @@ export interface RateLimit {
bucket: string | null bucket: string | null
} }
const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head']
export type MethodFunction = (
body?: unknown,
maxRetries?: number,
bucket?: string | null,
rawResponse?: boolean
) => Promise<any>
export interface APIMap extends MethodFunction {
get: APIMap
post: APIMap
patch: APIMap
put: APIMap
delete: APIMap
head: APIMap
[name: string]: APIMap
}
export const builder = (rest: RESTManager, acum = '/'): APIMap => {
const routes = {}
const proxy = new Proxy(routes, {
get: (_, p, __) => {
if (p === 'toString') return () => acum
if (METHODS.includes(String(p))) {
const method = ((rest as unknown) as {
[name: string]: MethodFunction
})[String(p)]
return async (...args: any[]) =>
await method.bind(rest)(
`${baseEndpoints.DISCORD_API_URL}/v${rest.version}${acum.substring(
0,
acum.length - 1
)}`,
...args
)
}
return builder(rest, acum + String(p) + '/')
}
})
return (proxy as unknown) as APIMap
}
export interface RESTOptions {
token?: string
headers?: { [name: string]: string | undefined }
canary?: boolean
}
export class RESTManager { export class RESTManager {
client?: Client client?: RESTOptions
queues: { [key: string]: QueuedItem[] } = {} queues: { [key: string]: QueuedItem[] } = {}
rateLimits = new Collection<string, RateLimit>() rateLimits = new Collection<string, RateLimit>()
globalRateLimit: boolean = false globalRateLimit: boolean = false
processing: boolean = false processing: boolean = false
version: number = 8
api: APIMap
constructor(client?: Client) { constructor(client?: RESTOptions) {
this.client = client this.client = client
this.api = builder(this)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.handleRateLimits() this.handleRateLimits()
} }
@ -160,10 +211,16 @@ export class RESTManager {
form.append('file', body.file.blob, body.file.name) form.append('file', body.file.blob, body.file.name)
form.append('payload_json', JSON.stringify({ ...body, file: undefined })) form.append('payload_json', JSON.stringify({ ...body, file: undefined }))
body.file = form 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' headers['Content-Type'] = 'application/json'
} }
if (this.client?.headers !== undefined)
Object.assign(headers, this.client.headers)
const data: { [name: string]: any } = { const data: { [name: string]: any } = {
headers, headers,
body: body?.file ?? JSON.stringify(body), body: body?.file ?? JSON.stringify(body),

View file

@ -14,10 +14,7 @@ import {
} from '../types/slash.ts' } from '../types/slash.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts' import { Client } from './client.ts'
import { RESTManager } from './rest.ts'
export interface SlashOptions {
enabled?: boolean
}
export class SlashCommand { export class SlashCommand {
slash: SlashCommandsManager slash: SlashCommandsManager
@ -47,20 +44,22 @@ export class SlashCommand {
} }
export class SlashCommandsManager { export class SlashCommandsManager {
client: Client
slash: SlashClient slash: SlashClient
constructor(client: Client) { get rest(): RESTManager {
this.client = client return this.slash.rest
this.slash = client.slash }
constructor(client: SlashClient) {
this.slash = client
} }
/** Get all Global Slash Commands */ /** Get all Global Slash Commands */
async all(): Promise<Collection<string, SlashCommand>> { async all(): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>() const col = new Collection<string, SlashCommand>()
const res = (await this.client.rest.get( const res = (await this.rest.get(
APPLICATION_COMMANDS(this.client.user?.id as string) APPLICATION_COMMANDS(this.slash.getID())
)) as SlashCommandPayload[] )) as SlashCommandPayload[]
if (!Array.isArray(res)) return col if (!Array.isArray(res)) return col
@ -78,9 +77,9 @@ export class SlashCommandsManager {
): Promise<Collection<string, SlashCommand>> { ): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>() const col = new Collection<string, SlashCommand>()
const res = (await this.client.rest.get( const res = (await this.rest.get(
APPLICATION_GUILD_COMMANDS( APPLICATION_GUILD_COMMANDS(
this.client.user?.id as string, this.slash.getID(),
typeof guild === 'string' ? guild : guild.id typeof guild === 'string' ? guild : guild.id
) )
)) as SlashCommandPayload[] )) as SlashCommandPayload[]
@ -100,11 +99,11 @@ export class SlashCommandsManager {
data: SlashCommandPartial, data: SlashCommandPartial,
guild?: Guild | string guild?: Guild | string
): Promise<SlashCommand> { ): Promise<SlashCommand> {
const payload = await this.client.rest.post( const payload = await this.rest.post(
guild === undefined guild === undefined
? APPLICATION_COMMANDS(this.client.user?.id as string) ? APPLICATION_COMMANDS(this.slash.getID())
: APPLICATION_GUILD_COMMANDS( : APPLICATION_GUILD_COMMANDS(
this.client.user?.id as string, this.slash.getID(),
typeof guild === 'string' ? guild : guild.id typeof guild === 'string' ? guild : guild.id
), ),
data data
@ -123,11 +122,11 @@ export class SlashCommandsManager {
data: SlashCommandPartial, data: SlashCommandPartial,
guild?: Guild | string guild?: Guild | string
): Promise<SlashCommandsManager> { ): Promise<SlashCommandsManager> {
await this.client.rest.patch( await this.rest.patch(
guild === undefined guild === undefined
? APPLICATION_COMMAND(this.client.user?.id as string, id) ? APPLICATION_COMMAND(this.slash.getID(), id)
: APPLICATION_GUILD_COMMAND( : APPLICATION_GUILD_COMMAND(
this.client.user?.id as string, this.slash.getID(),
typeof guild === 'string' ? guild : guild.id, typeof guild === 'string' ? guild : guild.id,
id id
), ),
@ -141,17 +140,31 @@ export class SlashCommandsManager {
id: string, id: string,
guild?: Guild | string guild?: Guild | string
): Promise<SlashCommandsManager> { ): Promise<SlashCommandsManager> {
await this.client.rest.delete( await this.rest.delete(
guild === undefined guild === undefined
? APPLICATION_COMMAND(this.client.user?.id as string, id) ? APPLICATION_COMMAND(this.slash.getID(), id)
: APPLICATION_GUILD_COMMAND( : APPLICATION_GUILD_COMMAND(
this.client.user?.id as string, this.slash.getID(),
typeof guild === 'string' ? guild : guild.id, typeof guild === 'string' ? guild : guild.id,
id id
) )
) )
return this return this
} }
/** Get a Slash Command (global or Guild) */
async get(id: string, guild?: Guild | string): Promise<SlashCommand> {
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 export type SlashCommandHandlerCallback = (interaction: Interaction) => any
@ -163,31 +176,61 @@ export interface SlashCommandHandler {
handler: SlashCommandHandlerCallback handler: SlashCommandHandlerCallback
} }
export interface SlashOptions {
id?: string | (() => string)
client?: Client
enabled?: boolean
token?: string
rest?: RESTManager
}
export class SlashClient { export class SlashClient {
client: Client id: string | (() => string)
client?: Client
token?: string
enabled: boolean = true enabled: boolean = true
commands: SlashCommandsManager commands: SlashCommandsManager
handlers: SlashCommandHandler[] = [] handlers: SlashCommandHandler[] = []
rest: RESTManager
constructor(client: Client, options?: SlashOptions) { constructor(options: SlashOptions) {
this.client = client let id = options.id
this.commands = new SlashCommandsManager(client) 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) { if (options !== undefined) {
this.enabled = options.enabled ?? true this.enabled = options.enabled ?? true
} }
if (this.client._decoratedSlash !== undefined) { if (this.client?._decoratedSlash !== undefined) {
this.client._decoratedSlash.forEach((e) => { this.client._decoratedSlash.forEach((e) => {
this.handlers.push(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) this._process(interaction)
) )
} }
getID(): string {
return typeof this.id === 'string' ? this.id : this.id()
}
/** Adds a new Slash Command Handler */ /** Adds a new Slash Command Handler */
handle(handler: SlashCommandHandler): SlashClient { handle(handler: SlashCommandHandler): SlashClient {
this.handlers.push(handler) this.handlers.push(handler)

View file

@ -60,12 +60,12 @@ class MyClient extends CommandClient {
} }
@subslash('cmd', 'sub-cmd-no-grp') @subslash('cmd', 'sub-cmd-no-grp')
subCmdNoGrp(d: Interaction): void { subCmdNoGroup(d: Interaction): void {
d.respond({ content: 'sub-cmd-no-group worked' }) d.respond({ content: 'sub-cmd-no-group worked' })
} }
@groupslash('cmd', 'sub-cmd-group', 'sub-cmd') @groupslash('cmd', 'sub-cmd-group', 'sub-cmd')
subCmdGrp(d: Interaction): void { subCmdGroup(d: Interaction): void {
d.respond({ content: 'sub-cmd-group worked' }) d.respond({ content: 'sub-cmd-group worked' })
} }
@ -74,14 +74,23 @@ class MyClient extends CommandClient {
console.log(d.name) console.log(d.name)
} }
@event()
raw(evt: string, d: any): void {
if (!evt.startsWith('APPLICATION')) return
console.log(evt, d)
}
@event() @event()
ready(): void { ready(): void {
console.log(`Logged in as ${this.user?.tag}!`) console.log(`Logged in as ${this.user?.tag}!`)
this.manager.init(this.user?.id as string) 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( // client.slash.commands.create(
// { // {
// name: 'cmd', // name: 'cmd',
// description: 'Parent command', // description: 'Parent command!',
// options: [ // options: [
// { // {
// name: 'sub-cmd-group', // name: 'sub-cmd-group',
@ -121,6 +130,7 @@ class MyClient extends CommandClient {
// }, // },
// '783319033205751809' // '783319033205751809'
// ) // )
// client.slash.commands.delete('788719077329207296', '783319033205751809')
} }
} }
@ -166,7 +176,7 @@ class VCExtension extends Extension {
await player.play(track) await player.play(track)
ctx.channel.send(`Now playing ${info.title}!`) ctx.channel.send(`Now playing ${info.title}!\nDebug Track: ${track}`)
} }
@command() @command()

6
src/test/slash-only.ts Normal file
View file

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