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

View file

@ -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<Application>
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<Application> {
const app = await this.rest.api.oauth2.applications['@me'].get()
return new Application(this, app)
}
/**
* This function is used for connecting to discord.

View file

@ -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 =
@ -51,15 +50,67 @@ export interface RateLimit {
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 {
client?: Client
client?: RESTOptions
queues: { [key: string]: QueuedItem[] } = {}
rateLimits = new Collection<string, RateLimit>()
globalRateLimit: boolean = false
processing: boolean = false
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
this.handleRateLimits()
}
@ -160,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),

View file

@ -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<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
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<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(
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<SlashCommand> {
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<SlashCommandsManager> {
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<SlashCommandsManager> {
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<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
@ -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)

View file

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