Merge pull request #66 from DjDeveloperr/slash
SlashClient & RESTManager can run alone and added APIMap
This commit is contained in:
commit
3695b81de6
6 changed files with 192 additions and 43 deletions
|
@ -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 }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
6
src/test/slash-only.ts
Normal 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)
|
Loading…
Reference in a new issue