fix
This commit is contained in:
commit
a1ead7e15e
169 changed files with 4849 additions and 2446 deletions
4
.github/workflows/deno.yml
vendored
4
.github/workflows/deno.yml
vendored
|
@ -20,14 +20,14 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
deno: ['v1.x', 'nightly']
|
deno: ['v1.x', 'canary']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup repo
|
- name: Setup repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denolib/setup-deno@v2.3.0
|
uses: denoland/setup-deno@main
|
||||||
with:
|
with:
|
||||||
deno-version: ${{ matrix.deno }} # tests across multiple Deno versions
|
deno-version: ${{ matrix.deno }} # tests across multiple Deno versions
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -109,6 +109,7 @@ yarn.lock
|
||||||
|
|
||||||
# PRIVACY XDDDD
|
# PRIVACY XDDDD
|
||||||
src/test/config.ts
|
src/test/config.ts
|
||||||
|
test/config.ts
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# macOS is shit xD
|
# macOS is shit xD
|
||||||
|
|
|
@ -59,7 +59,7 @@ client.on('ready', () => {
|
||||||
// Listen for event whenever a Message is sent
|
// Listen for event whenever a Message is sent
|
||||||
client.on('messageCreate', (msg: Message): void => {
|
client.on('messageCreate', (msg: Message): void => {
|
||||||
if (msg.content === '!ping') {
|
if (msg.content === '!ping') {
|
||||||
msg.channel.send(`Pong! WS Ping: ${client.ping}`)
|
msg.channel.send(`Pong! WS Ping: ${client.gateway.ping}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class PingCommand extends Command {
|
||||||
name = 'ping'
|
name = 'ping'
|
||||||
|
|
||||||
execute(ctx: CommandContext) {
|
execute(ctx: CommandContext) {
|
||||||
ctx.message.reply(`pong! Ping: ${ctx.client.ping}ms`)
|
ctx.message.reply(`pong! Ping: ${ctx.client.gateway.ping}ms`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ Documentation is available for `main` (branch) and `stable` (release).
|
||||||
|
|
||||||
## Found a bug or want support? Join our discord server!
|
## Found a bug or want support? Join our discord server!
|
||||||
|
|
||||||
[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/WVN2JF2FRv)
|
[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/harmonyland)
|
||||||
|
|
||||||
## Maintainer
|
## Maintainer
|
||||||
|
|
||||||
|
|
131
deploy.ts
Normal file
131
deploy.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import {
|
||||||
|
SlashCommandsManager,
|
||||||
|
SlashClient,
|
||||||
|
SlashCommandHandlerCallback,
|
||||||
|
SlashCommandHandler
|
||||||
|
} from './src/interactions/mod.ts'
|
||||||
|
import { InteractionResponseType, InteractionType } from './src/types/slash.ts'
|
||||||
|
|
||||||
|
export interface DeploySlashInitOptions {
|
||||||
|
env?: boolean
|
||||||
|
publicKey?: string
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current Slash Client being used to handle commands */
|
||||||
|
let client: SlashClient
|
||||||
|
/** Manage Slash Commands right in Deploy */
|
||||||
|
let commands: SlashCommandsManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Slash Commands Handler for [Deno Deploy](https://deno.com/deploy).
|
||||||
|
* Easily create Serverless Slash Commands on the fly.
|
||||||
|
*
|
||||||
|
* **Examples**
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* init({
|
||||||
|
* publicKey: "my public key",
|
||||||
|
* token: "my bot's token", // only required if you want to manage slash commands in code
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* // takes up `PUBLIC_KEY` and `TOKEN` from ENV
|
||||||
|
* init({ env: true })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param options Initialization options
|
||||||
|
*/
|
||||||
|
export function init(options: { env: boolean }): void
|
||||||
|
export function init(options: { publicKey: string; token?: string }): void
|
||||||
|
export function init(options: DeploySlashInitOptions): void {
|
||||||
|
if (client !== undefined) throw new Error('Already initialized')
|
||||||
|
if (options.env === true) {
|
||||||
|
options.publicKey = Deno.env.get('PUBLIC_KEY')
|
||||||
|
options.token = Deno.env.get('TOKEN')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.publicKey === undefined)
|
||||||
|
throw new Error('Public Key not provided')
|
||||||
|
|
||||||
|
client = new SlashClient({
|
||||||
|
token: options.token,
|
||||||
|
publicKey: options.publicKey
|
||||||
|
})
|
||||||
|
|
||||||
|
commands = client.commands
|
||||||
|
|
||||||
|
const cb = async (evt: {
|
||||||
|
respondWith: CallableFunction
|
||||||
|
request: Request
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// we have to wrap because there are some weird scope errors
|
||||||
|
const d = await client.verifyFetchEvent({
|
||||||
|
respondWith: (...args: any[]) => evt.respondWith(...args),
|
||||||
|
request: evt.request
|
||||||
|
})
|
||||||
|
if (d === false) {
|
||||||
|
await evt.respondWith(
|
||||||
|
new Response('Not Authorized', {
|
||||||
|
status: 400
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.type === InteractionType.PING) {
|
||||||
|
await d.respond({ type: InteractionResponseType.PONG })
|
||||||
|
client.emit('ping')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await (client as any)._process(d)
|
||||||
|
} catch (e) {
|
||||||
|
await client.emit('interactionError', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener('fetch', cb as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Slash Command handler.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* handle("ping", (interaction) => {
|
||||||
|
* interaction.reply("Pong!")
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Also supports Sub Command and Group handling out of the box!
|
||||||
|
* ```ts
|
||||||
|
* handle("command-name group-name sub-command", (i) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* handle("command-name sub-command", (i) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param cmd Command to handle. Either Handler object or command name followed by handler function in next parameter.
|
||||||
|
* @param handler Handler function (required if previous argument was command name)
|
||||||
|
*/
|
||||||
|
export function handle(
|
||||||
|
cmd: string | SlashCommandHandler,
|
||||||
|
handler?: SlashCommandHandlerCallback
|
||||||
|
): void {
|
||||||
|
if (client === undefined)
|
||||||
|
throw new Error('Slash Client not initialized. Call `init` first')
|
||||||
|
client.handle(cmd, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { commands, client }
|
||||||
|
export * from './src/types/slash.ts'
|
||||||
|
export * from './src/structures/slash.ts'
|
||||||
|
export * from './src/interactions/mod.ts'
|
||||||
|
export * from './src/types/channel.ts'
|
5
deps.ts
5
deps.ts
|
@ -1,11 +1,6 @@
|
||||||
export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts'
|
export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts'
|
||||||
export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts'
|
export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts'
|
||||||
export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts'
|
export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts'
|
||||||
export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts'
|
|
||||||
export type {
|
|
||||||
Redis,
|
|
||||||
RedisConnectOptions
|
|
||||||
} from 'https://deno.land/x/redis@v0.14.1/mod.ts'
|
|
||||||
export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts'
|
export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts'
|
||||||
export { join } from 'https://deno.land/std@0.86.0/path/mod.ts'
|
export { join } from 'https://deno.land/std@0.86.0/path/mod.ts'
|
||||||
export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0'
|
export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0'
|
||||||
|
|
71
mod.ts
71
mod.ts
|
@ -1,14 +1,18 @@
|
||||||
export { GatewayIntents } from './src/types/gateway.ts'
|
export { GatewayIntents } from './src/types/gateway.ts'
|
||||||
export { Base } from './src/structures/base.ts'
|
export { Base } from './src/structures/base.ts'
|
||||||
export { Gateway } from './src/gateway/index.ts'
|
export { Gateway } from './src/gateway/mod.ts'
|
||||||
export type { GatewayTypedEvents } from './src/gateway/index.ts'
|
export type { GatewayTypedEvents } from './src/gateway/mod.ts'
|
||||||
export type { ClientEvents } from './src/gateway/handlers/index.ts'
|
export type { ClientEvents } from './src/gateway/handlers/mod.ts'
|
||||||
export * from './src/models/client.ts'
|
export * from './src/client/mod.ts'
|
||||||
export * from './src/models/slashClient.ts'
|
export * from './src/interactions/mod.ts'
|
||||||
export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts'
|
export {
|
||||||
export type { RequestHeaders } from './src/models/rest.ts'
|
RESTManager,
|
||||||
export type { RESTOptions } from './src/models/rest.ts'
|
TokenType,
|
||||||
export * from './src/models/cacheAdapter.ts'
|
HttpResponseCode,
|
||||||
|
DiscordAPIError
|
||||||
|
} from './src/rest/mod.ts'
|
||||||
|
export * from './src/rest/mod.ts'
|
||||||
|
export * from './src/cache/adapter.ts'
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandBuilder,
|
CommandBuilder,
|
||||||
|
@ -16,16 +20,16 @@ export {
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
CategoriesManager,
|
CategoriesManager,
|
||||||
CommandsLoader
|
CommandsLoader
|
||||||
} from './src/models/command.ts'
|
} from './src/commands/command.ts'
|
||||||
export type { CommandContext, CommandOptions } from './src/models/command.ts'
|
export type { CommandContext, CommandOptions } from './src/commands/command.ts'
|
||||||
export {
|
export {
|
||||||
Extension,
|
Extension,
|
||||||
ExtensionCommands,
|
ExtensionCommands,
|
||||||
ExtensionsManager
|
ExtensionsManager
|
||||||
} from './src/models/extensions.ts'
|
} from './src/commands/extension.ts'
|
||||||
export { SlashModule } from './src/models/slashModule.ts'
|
export { SlashModule } from './src/interactions/slashModule.ts'
|
||||||
export { CommandClient, command } from './src/models/commandClient.ts'
|
export { CommandClient, command } from './src/commands/client.ts'
|
||||||
export type { CommandClientOptions } from './src/models/commandClient.ts'
|
export type { CommandClientOptions } from './src/commands/client.ts'
|
||||||
export { BaseManager } from './src/managers/base.ts'
|
export { BaseManager } from './src/managers/base.ts'
|
||||||
export { BaseChildManager } from './src/managers/baseChild.ts'
|
export { BaseChildManager } from './src/managers/baseChild.ts'
|
||||||
export { ChannelsManager } from './src/managers/channels.ts'
|
export { ChannelsManager } from './src/managers/channels.ts'
|
||||||
|
@ -45,7 +49,7 @@ export { RolesManager } from './src/managers/roles.ts'
|
||||||
export { UsersManager } from './src/managers/users.ts'
|
export { UsersManager } from './src/managers/users.ts'
|
||||||
export { InviteManager } from './src/managers/invites.ts'
|
export { InviteManager } from './src/managers/invites.ts'
|
||||||
export { Application } from './src/structures/application.ts'
|
export { Application } from './src/structures/application.ts'
|
||||||
// export { ImageURL } from './src/structures/cdn.ts'
|
export { ImageURL } from './src/structures/cdn.ts'
|
||||||
export { Channel, GuildChannel } from './src/structures/channel.ts'
|
export { Channel, GuildChannel } from './src/structures/channel.ts'
|
||||||
export type { EditOverwriteOptions } from './src/structures/channel.ts'
|
export type { EditOverwriteOptions } from './src/structures/channel.ts'
|
||||||
export { DMChannel } from './src/structures/dmChannel.ts'
|
export { DMChannel } from './src/structures/dmChannel.ts'
|
||||||
|
@ -63,7 +67,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts'
|
||||||
export { VoiceChannel } from './src/structures/guildVoiceChannel.ts'
|
export { VoiceChannel } from './src/structures/guildVoiceChannel.ts'
|
||||||
export { Invite } from './src/structures/invite.ts'
|
export { Invite } from './src/structures/invite.ts'
|
||||||
export * from './src/structures/member.ts'
|
export * from './src/structures/member.ts'
|
||||||
export { Message, MessageAttachment } from './src/structures/message.ts'
|
export {
|
||||||
|
Message,
|
||||||
|
MessageAttachment,
|
||||||
|
MessageInteraction
|
||||||
|
} from './src/structures/message.ts'
|
||||||
export { MessageMentions } from './src/structures/messageMentions.ts'
|
export { MessageMentions } from './src/structures/messageMentions.ts'
|
||||||
export {
|
export {
|
||||||
Presence,
|
Presence,
|
||||||
|
@ -88,7 +96,7 @@ export { Intents } from './src/utils/intents.ts'
|
||||||
export * from './src/utils/permissions.ts'
|
export * from './src/utils/permissions.ts'
|
||||||
export { UserFlagsManager } from './src/utils/userFlags.ts'
|
export { UserFlagsManager } from './src/utils/userFlags.ts'
|
||||||
export { HarmonyEventEmitter } from './src/utils/events.ts'
|
export { HarmonyEventEmitter } from './src/utils/events.ts'
|
||||||
export type { EveryChannelTypes } from './src/utils/getChannelByType.ts'
|
export type { EveryChannelTypes } from './src/utils/channel.ts'
|
||||||
export * from './src/utils/bitfield.ts'
|
export * from './src/utils/bitfield.ts'
|
||||||
export type {
|
export type {
|
||||||
ActivityGame,
|
ActivityGame,
|
||||||
|
@ -96,7 +104,15 @@ export type {
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusType
|
StatusType
|
||||||
} from './src/types/presence.ts'
|
} from './src/types/presence.ts'
|
||||||
export { ChannelTypes } from './src/types/channel.ts'
|
export {
|
||||||
|
ChannelTypes,
|
||||||
|
OverwriteType,
|
||||||
|
OverrideType
|
||||||
|
} from './src/types/channel.ts'
|
||||||
|
export type {
|
||||||
|
OverwriteAsOptions,
|
||||||
|
OverwritePayload
|
||||||
|
} from './src/types/channel.ts'
|
||||||
export type { ApplicationPayload } from './src/types/application.ts'
|
export type { ApplicationPayload } from './src/types/application.ts'
|
||||||
export type { ImageFormats, ImageSize } from './src/types/cdn.ts'
|
export type { ImageFormats, ImageSize } from './src/types/cdn.ts'
|
||||||
export type {
|
export type {
|
||||||
|
@ -110,9 +126,18 @@ export type {
|
||||||
GuildVoiceChannelPayload,
|
GuildVoiceChannelPayload,
|
||||||
GroupDMChannelPayload,
|
GroupDMChannelPayload,
|
||||||
MessageOptions,
|
MessageOptions,
|
||||||
|
MessagePayload,
|
||||||
|
MessageInteractionPayload,
|
||||||
|
MessageReference,
|
||||||
|
MessageActivity,
|
||||||
|
MessageActivityTypes,
|
||||||
|
MessageApplication,
|
||||||
|
MessageFlags,
|
||||||
|
MessageStickerFormatTypes,
|
||||||
|
MessageStickerPayload,
|
||||||
|
MessageTypes,
|
||||||
OverwriteAsArg,
|
OverwriteAsArg,
|
||||||
Overwrite,
|
Overwrite
|
||||||
OverwriteAsOptions
|
|
||||||
} from './src/types/channel.ts'
|
} from './src/types/channel.ts'
|
||||||
export type { EmojiPayload } from './src/types/emoji.ts'
|
export type { EmojiPayload } from './src/types/emoji.ts'
|
||||||
export { Verification } from './src/types/guild.ts'
|
export { Verification } from './src/types/guild.ts'
|
||||||
|
@ -145,7 +170,9 @@ export type { UserPayload } from './src/types/user.ts'
|
||||||
export { UserFlags } from './src/types/userFlags.ts'
|
export { UserFlags } from './src/types/userFlags.ts'
|
||||||
export type { VoiceStatePayload } from './src/types/voice.ts'
|
export type { VoiceStatePayload } from './src/types/voice.ts'
|
||||||
export type { WebhookPayload } from './src/types/webhook.ts'
|
export type { WebhookPayload } from './src/types/webhook.ts'
|
||||||
export * from './src/models/collectors.ts'
|
export * from './src/client/collectors.ts'
|
||||||
|
export type { Dict } from './src/utils/dict.ts'
|
||||||
|
export * from './src/cache/redis.ts'
|
||||||
export { ColorUtil } from './src/utils/colorutil.ts'
|
export { ColorUtil } from './src/utils/colorutil.ts'
|
||||||
export type { Colors } from './src/utils/colorutil.ts'
|
export type { Colors } from './src/utils/colorutil.ts'
|
||||||
export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts'
|
export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts'
|
22
src/cache/adapter.ts
vendored
Normal file
22
src/cache/adapter.ts
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony.
|
||||||
|
*
|
||||||
|
* Methods can return Promises too.
|
||||||
|
*/
|
||||||
|
export interface ICacheAdapter {
|
||||||
|
/** Gets a key from a Cache */
|
||||||
|
get: (cacheName: string, key: string) => Promise<any> | any
|
||||||
|
/** Sets a key to value in a Cache Name with optional expire value in MS */
|
||||||
|
set: (
|
||||||
|
cacheName: string,
|
||||||
|
key: string,
|
||||||
|
value: any,
|
||||||
|
expire?: number
|
||||||
|
) => Promise<any> | any
|
||||||
|
/** Deletes a key from a Cache */
|
||||||
|
delete: (cacheName: string, key: string) => Promise<boolean> | boolean
|
||||||
|
/** Gets array of all values in a Cache */
|
||||||
|
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined>
|
||||||
|
/** Entirely deletes a Cache */
|
||||||
|
deleteCache: (cacheName: string) => any
|
||||||
|
}
|
50
src/cache/default.ts
vendored
Normal file
50
src/cache/default.ts
vendored
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
import type { ICacheAdapter } from './adapter.ts'
|
||||||
|
|
||||||
|
/** Default Cache Adapter for in-memory caching. */
|
||||||
|
export class DefaultCacheAdapter implements ICacheAdapter {
|
||||||
|
data: {
|
||||||
|
[name: string]: Collection<string, any>
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
async get(cacheName: string, key: string): Promise<undefined | any> {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) return
|
||||||
|
return cache.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(
|
||||||
|
cacheName: string,
|
||||||
|
key: string,
|
||||||
|
value: any,
|
||||||
|
expire?: number
|
||||||
|
): Promise<any> {
|
||||||
|
let cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) {
|
||||||
|
this.data[cacheName] = new Collection()
|
||||||
|
cache = this.data[cacheName]
|
||||||
|
}
|
||||||
|
cache.set(key, value)
|
||||||
|
if (expire !== undefined)
|
||||||
|
setTimeout(() => {
|
||||||
|
cache.delete(key)
|
||||||
|
}, expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(cacheName: string, key: string): Promise<boolean> {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) return false
|
||||||
|
return cache.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async array(cacheName: string): Promise<any[] | undefined> {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (cache === undefined) return
|
||||||
|
return cache.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCache(cacheName: string): Promise<boolean> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
|
return delete this.data[cacheName]
|
||||||
|
}
|
||||||
|
}
|
4
src/cache/mod.ts
vendored
Normal file
4
src/cache/mod.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './adapter.ts'
|
||||||
|
export * from './default.ts'
|
||||||
|
// Not exported by default
|
||||||
|
// export * from './redis.ts'
|
80
src/models/cacheAdapter.ts → src/cache/redis.ts
vendored
80
src/models/cacheAdapter.ts → src/cache/redis.ts
vendored
|
@ -1,76 +1,10 @@
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { ICacheAdapter } from './adapter.ts'
|
||||||
import { connect, Redis, RedisConnectOptions } from '../../deps.ts'
|
// Not in deps.ts to allow optional dep loading
|
||||||
|
import {
|
||||||
/**
|
connect,
|
||||||
* ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony.
|
Redis,
|
||||||
*
|
RedisConnectOptions
|
||||||
* Methods can return Promises too.
|
} from 'https://deno.land/x/redis@v0.14.1/mod.ts'
|
||||||
*/
|
|
||||||
export interface ICacheAdapter {
|
|
||||||
/** Gets a key from a Cache */
|
|
||||||
get: (cacheName: string, key: string) => Promise<any> | any
|
|
||||||
/** Sets a key to value in a Cache Name with optional expire value in MS */
|
|
||||||
set: (
|
|
||||||
cacheName: string,
|
|
||||||
key: string,
|
|
||||||
value: any,
|
|
||||||
expire?: number
|
|
||||||
) => Promise<any> | any
|
|
||||||
/** Deletes a key from a Cache */
|
|
||||||
delete: (cacheName: string, key: string) => Promise<boolean> | boolean
|
|
||||||
/** Gets array of all values in a Cache */
|
|
||||||
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined>
|
|
||||||
/** Entirely deletes a Cache */
|
|
||||||
deleteCache: (cacheName: string) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Default Cache Adapter for in-memory caching. */
|
|
||||||
export class DefaultCacheAdapter implements ICacheAdapter {
|
|
||||||
data: {
|
|
||||||
[name: string]: Collection<string, any>
|
|
||||||
} = {}
|
|
||||||
|
|
||||||
async get(cacheName: string, key: string): Promise<undefined | any> {
|
|
||||||
const cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) return
|
|
||||||
return cache.get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(
|
|
||||||
cacheName: string,
|
|
||||||
key: string,
|
|
||||||
value: any,
|
|
||||||
expire?: number
|
|
||||||
): Promise<any> {
|
|
||||||
let cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) {
|
|
||||||
this.data[cacheName] = new Collection()
|
|
||||||
cache = this.data[cacheName]
|
|
||||||
}
|
|
||||||
cache.set(key, value)
|
|
||||||
if (expire !== undefined)
|
|
||||||
setTimeout(() => {
|
|
||||||
cache.delete(key)
|
|
||||||
}, expire)
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(cacheName: string, key: string): Promise<boolean> {
|
|
||||||
const cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) return false
|
|
||||||
return cache.delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
async array(cacheName: string): Promise<any[] | undefined> {
|
|
||||||
const cache = this.data[cacheName]
|
|
||||||
if (cache === undefined) return
|
|
||||||
return cache.array()
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteCache(cacheName: string): Promise<boolean> {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
return delete this.data[cacheName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Redis Cache Adapter for using Redis as a cache-provider. */
|
/** Redis Cache Adapter for using Redis as a cache-provider. */
|
||||||
export class RedisCacheAdapter implements ICacheAdapter {
|
export class RedisCacheAdapter implements ICacheAdapter {
|
|
@ -1,29 +1,28 @@
|
||||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { GatewayIntents } from '../types/gateway.ts'
|
import { GatewayIntents } from '../types/gateway.ts'
|
||||||
import { Gateway } from '../gateway/index.ts'
|
import { Gateway } from '../gateway/mod.ts'
|
||||||
import { RESTManager, RESTOptions, TokenType } from './rest.ts'
|
import { RESTManager, RESTOptions, TokenType } from '../rest/mod.ts'
|
||||||
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
|
import { DefaultCacheAdapter, ICacheAdapter } from '../cache/mod.ts'
|
||||||
import { UsersManager } from '../managers/users.ts'
|
import { UsersManager } from '../managers/users.ts'
|
||||||
import { GuildManager } from '../managers/guilds.ts'
|
import { GuildManager } from '../managers/guilds.ts'
|
||||||
import { ChannelsManager } from '../managers/channels.ts'
|
import { ChannelsManager } from '../managers/channels.ts'
|
||||||
import { ClientPresence } from '../structures/presence.ts'
|
import { ClientPresence } from '../structures/presence.ts'
|
||||||
import { EmojisManager } from '../managers/emojis.ts'
|
import { EmojisManager } from '../managers/emojis.ts'
|
||||||
import { ActivityGame, ClientActivity } from '../types/presence.ts'
|
import { ActivityGame, ClientActivity } from '../types/presence.ts'
|
||||||
import { Extension } from './extensions.ts'
|
import type { Extension } from '../commands/extension.ts'
|
||||||
import { SlashClient } from './slashClient.ts'
|
import { SlashClient } from '../interactions/slashClient.ts'
|
||||||
import { Interaction } from '../structures/slash.ts'
|
import type { Interaction } from '../structures/slash.ts'
|
||||||
import { SlashModule } from './slashModule.ts'
|
|
||||||
import { ShardManager } from './shard.ts'
|
import { ShardManager } from './shard.ts'
|
||||||
import { Application } from '../structures/application.ts'
|
import { Application } from '../structures/application.ts'
|
||||||
import { Invite } from '../structures/invite.ts'
|
import { Invite } from '../structures/invite.ts'
|
||||||
import { INVITE } from '../types/endpoint.ts'
|
import { INVITE } from '../types/endpoint.ts'
|
||||||
import { ClientEvents } from '../gateway/handlers/index.ts'
|
import type { ClientEvents } from '../gateway/handlers/mod.ts'
|
||||||
import type { Collector } from './collectors.ts'
|
import type { Collector } from './collectors.ts'
|
||||||
import { HarmonyEventEmitter } from '../utils/events.ts'
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
import { VoiceRegion } from '../types/voice.ts'
|
import type { VoiceRegion } from '../types/voice.ts'
|
||||||
import { fetchAuto } from '../../deps.ts'
|
import { fetchAuto } from '../../deps.ts'
|
||||||
import { DMChannel } from '../structures/dmChannel.ts'
|
import type { DMChannel } from '../structures/dmChannel.ts'
|
||||||
import { Template } from '../structures/template.ts'
|
import { Template } from '../structures/template.ts'
|
||||||
|
|
||||||
/** OS related properties sent with Gateway Identify */
|
/** OS related properties sent with Gateway Identify */
|
||||||
|
@ -72,15 +71,13 @@ export interface ClientOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord Client.
|
* Harmony Client. Provides high-level interface over the REST and WebSocket API.
|
||||||
*/
|
*/
|
||||||
export class Client extends HarmonyEventEmitter<ClientEvents> {
|
export class Client extends HarmonyEventEmitter<ClientEvents> {
|
||||||
/** REST Manager - used to make all requests */
|
/** REST Manager - used to make all requests */
|
||||||
rest: RESTManager
|
rest: RESTManager
|
||||||
/** User which Client logs in to, undefined until logs in */
|
/** User which Client logs in to, undefined until logs in */
|
||||||
user?: User
|
user?: User
|
||||||
/** WebSocket ping of Client */
|
|
||||||
ping = 0
|
|
||||||
/** Token of the Bot/User */
|
/** Token of the Bot/User */
|
||||||
token?: string
|
token?: string
|
||||||
/** Cache Adapter */
|
/** Cache Adapter */
|
||||||
|
@ -149,9 +146,9 @@ export class Client extends HarmonyEventEmitter<ClientEvents> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get Shard 0's Gateway */
|
||||||
get gateway(): Gateway {
|
get gateway(): Gateway {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
return this.shards.list.get('0')!
|
||||||
return this.shards.list.get('0') as Gateway
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationID?: string
|
applicationID?: string
|
||||||
|
@ -290,7 +287,10 @@ export class Client extends HarmonyEventEmitter<ClientEvents> {
|
||||||
* @param token Your token. This is required if not given in ClientOptions.
|
* @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.
|
* @param intents Gateway intents in array. This is required if not given in ClientOptions.
|
||||||
*/
|
*/
|
||||||
async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> {
|
async connect(
|
||||||
|
token?: string,
|
||||||
|
intents?: Array<GatewayIntents | keyof typeof GatewayIntents>
|
||||||
|
): Promise<Client> {
|
||||||
token ??= this.token
|
token ??= this.token
|
||||||
if (token === undefined) throw new Error('No Token Provided')
|
if (token === undefined) throw new Error('No Token Provided')
|
||||||
this.token = token
|
this.token = token
|
||||||
|
@ -302,7 +302,9 @@ export class Client extends HarmonyEventEmitter<ClientEvents> {
|
||||||
} else if (intents === undefined && this.intents !== undefined) {
|
} else if (intents === undefined && this.intents !== undefined) {
|
||||||
intents = this.intents
|
intents = this.intents
|
||||||
} else if (intents !== undefined && this.intents === undefined) {
|
} else if (intents !== undefined && this.intents === undefined) {
|
||||||
this.intents = intents
|
this.intents = intents.map((e) =>
|
||||||
|
typeof e === 'string' ? GatewayIntents[e] : e
|
||||||
|
)
|
||||||
} else throw new Error('No Gateway Intents were provided')
|
} else throw new Error('No Gateway Intents were provided')
|
||||||
|
|
||||||
this.rest.token = token
|
this.rest.token = token
|
||||||
|
@ -436,59 +438,3 @@ export function event(name?: keyof ClientEvents) {
|
||||||
client._decoratedEvents[key] = listener
|
client._decoratedEvents[key] = listener
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Decorator to create a Slash Command handler */
|
|
||||||
export function slash(name?: string, guild?: string) {
|
|
||||||
return function (client: Client | SlashClient | SlashModule, prop: string) {
|
|
||||||
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
|
||||||
const item = (client as { [name: string]: any })[prop]
|
|
||||||
if (typeof item !== 'function') {
|
|
||||||
throw new Error('@slash decorator requires a function')
|
|
||||||
} else
|
|
||||||
client._decoratedSlash.push({
|
|
||||||
name: name ?? prop,
|
|
||||||
guild,
|
|
||||||
handler: item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decorator to create a Sub-Slash Command handler */
|
|
||||||
export function subslash(parent: string, name?: string, guild?: string) {
|
|
||||||
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
|
||||||
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
|
||||||
const item = (client as { [name: string]: any })[prop]
|
|
||||||
if (typeof item !== 'function') {
|
|
||||||
throw new Error('@subslash decorator requires a function')
|
|
||||||
} else
|
|
||||||
client._decoratedSlash.push({
|
|
||||||
parent,
|
|
||||||
name: name ?? prop,
|
|
||||||
guild,
|
|
||||||
handler: item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decorator to create a Grouped Slash Command handler */
|
|
||||||
export function groupslash(
|
|
||||||
parent: string,
|
|
||||||
group: string,
|
|
||||||
name?: string,
|
|
||||||
guild?: string
|
|
||||||
) {
|
|
||||||
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
|
||||||
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
|
||||||
const item = (client as { [name: string]: any })[prop]
|
|
||||||
if (typeof item !== 'function') {
|
|
||||||
throw new Error('@groupslash decorator requires a function')
|
|
||||||
} else
|
|
||||||
client._decoratedSlash.push({
|
|
||||||
group,
|
|
||||||
parent,
|
|
||||||
name: name ?? prop,
|
|
||||||
guild,
|
|
||||||
handler: item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import type { Client } from './client.ts'
|
import type { Client } from '../client/client.ts'
|
||||||
import { HarmonyEventEmitter } from '../utils/events.ts'
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
|
||||||
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
|
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
|
3
src/client/mod.ts
Normal file
3
src/client/mod.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './client.ts'
|
||||||
|
export * from './collectors.ts'
|
||||||
|
export * from './shard.ts'
|
|
@ -1,7 +1,7 @@
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import type { Client } from './client.ts'
|
import type { Client } from './client.ts'
|
||||||
import { RESTManager } from './rest.ts'
|
import { RESTManager } from '../rest/mod.ts'
|
||||||
import { Gateway } from '../gateway/index.ts'
|
import { Gateway } from '../gateway/mod.ts'
|
||||||
import { HarmonyEventEmitter } from '../utils/events.ts'
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
import { GatewayEvents } from '../types/gateway.ts'
|
import { GatewayEvents } from '../types/gateway.ts'
|
||||||
import { delay } from '../utils/delay.ts'
|
import { delay } from '../utils/delay.ts'
|
||||||
|
@ -61,10 +61,24 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
|
||||||
let shardCount: number
|
let shardCount: number
|
||||||
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
|
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
|
||||||
else {
|
else {
|
||||||
if (this.client.shardCount === 'auto') {
|
if (
|
||||||
|
this.client.shardCount === 'auto' &&
|
||||||
|
this.client.fetchGatewayInfo !== false
|
||||||
|
) {
|
||||||
|
this.debug('Fetch /gateway/bot...')
|
||||||
const info = await this.client.rest.api.gateway.bot.get()
|
const info = await this.client.rest.api.gateway.bot.get()
|
||||||
|
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`)
|
||||||
shardCount = info.shards as number
|
shardCount = info.shards as number
|
||||||
} else shardCount = this.client.shardCount ?? 1
|
} else
|
||||||
|
shardCount =
|
||||||
|
typeof this.client.shardCount === 'string'
|
||||||
|
? 1
|
||||||
|
: this.client.shardCount ?? 1
|
||||||
}
|
}
|
||||||
this.cachedShardCount = shardCount
|
this.cachedShardCount = shardCount
|
||||||
return this.cachedShardCount
|
return this.cachedShardCount
|
||||||
|
@ -79,8 +93,6 @@ export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
|
||||||
const shardCount = await this.getShardCount()
|
const shardCount = await this.getShardCount()
|
||||||
|
|
||||||
const gw = new Gateway(this.client, [Number(id), shardCount])
|
const gw = new Gateway(this.client, [Number(id), shardCount])
|
||||||
gw.token = this.client.token
|
|
||||||
gw.intents = this.client.intents
|
|
||||||
this.list.set(id.toString(), gw)
|
this.list.set(id.toString(), gw)
|
||||||
|
|
||||||
gw.initWebsocket()
|
gw.initWebsocket()
|
|
@ -1,6 +1,6 @@
|
||||||
import { Message } from '../structures/message.ts'
|
import type { Message } from '../structures/message.ts'
|
||||||
import { GuildTextBasedChannel } from '../structures/guildTextChannel.ts'
|
import type { GuildTextBasedChannel } from '../structures/guildTextChannel.ts'
|
||||||
import { Client, ClientOptions } from './client.ts'
|
import { Client, ClientOptions } from '../client/mod.ts'
|
||||||
import {
|
import {
|
||||||
CategoriesManager,
|
CategoriesManager,
|
||||||
Command,
|
Command,
|
||||||
|
@ -9,7 +9,7 @@ import {
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
parseCommand
|
parseCommand
|
||||||
} from './command.ts'
|
} from './command.ts'
|
||||||
import { Extension, ExtensionsManager } from './extensions.ts'
|
import { Extension, ExtensionsManager } from './extension.ts'
|
||||||
|
|
||||||
type PrefixReturnType = string | string[] | Promise<string | string[]>
|
type PrefixReturnType = string | string[] | Promise<string | string[]>
|
||||||
|
|
||||||
|
@ -43,6 +43,11 @@ export interface CommandClientOptions extends ClientOptions {
|
||||||
caseSensitive?: boolean
|
caseSensitive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Harmony Client with extended functionality for Message based Commands parsing and handling.
|
||||||
|
*
|
||||||
|
* See SlashClient (`Client#slash`) for more info about Slash Commands.
|
||||||
|
*/
|
||||||
export class CommandClient extends Client implements CommandClientOptions {
|
export class CommandClient extends Client implements CommandClientOptions {
|
||||||
prefix: string | string[]
|
prefix: string | string[]
|
||||||
mentionPrefix: boolean
|
mentionPrefix: boolean
|
||||||
|
@ -362,15 +367,19 @@ export class CommandClient extends Client implements CommandClientOptions {
|
||||||
const result = await command.execute(ctx)
|
const result = await command.execute(ctx)
|
||||||
await command.afterExecute(ctx, result)
|
await command.afterExecute(ctx, result)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await command
|
try {
|
||||||
.onError(ctx, e)
|
await command.onError(ctx, e)
|
||||||
.catch((e: Error) => this.emit('commandError', ctx, e))
|
} catch (e) {
|
||||||
|
this.emit('commandError', ctx, e)
|
||||||
|
}
|
||||||
this.emit('commandError', ctx, e)
|
this.emit('commandError', ctx, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Command decorator */
|
/**
|
||||||
|
* Command decorator. Decorates the function with optional metadata as a Command registered upon constructing class.
|
||||||
|
*/
|
||||||
export function command(options?: CommandOptions) {
|
export function command(options?: CommandOptions) {
|
||||||
return function (target: CommandClient | Extension, name: string) {
|
return function (target: CommandClient | Extension, name: string) {
|
||||||
if (target._decoratedCommands === undefined) target._decoratedCommands = {}
|
if (target._decoratedCommands === undefined) target._decoratedCommands = {}
|
|
@ -1,10 +1,10 @@
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
import type { Message } from '../structures/message.ts'
|
||||||
import { TextChannel } from '../structures/textChannel.ts'
|
import type { TextChannel } from '../structures/textChannel.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { CommandClient } from './commandClient.ts'
|
import type { CommandClient } from './client.ts'
|
||||||
import { Extension } from './extensions.ts'
|
import type { Extension } from './extension.ts'
|
||||||
import { join, walk } from '../../deps.ts'
|
import { join, walk } from '../../deps.ts'
|
||||||
|
|
||||||
export interface CommandContext {
|
export interface CommandContext {
|
||||||
|
@ -72,6 +72,8 @@ export interface CommandOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Command implements CommandOptions {
|
export class Command implements CommandOptions {
|
||||||
|
static meta?: CommandOptions
|
||||||
|
|
||||||
name: string = ''
|
name: string = ''
|
||||||
description?: string
|
description?: string
|
||||||
category?: string
|
category?: string
|
||||||
|
@ -298,6 +300,8 @@ export class CommandsLoader {
|
||||||
/**
|
/**
|
||||||
* Load a Command from file.
|
* Load a Command from file.
|
||||||
*
|
*
|
||||||
|
* NOTE: Relative paths resolve from cwd
|
||||||
|
*
|
||||||
* @param filePath Path of Command file.
|
* @param filePath Path of Command file.
|
||||||
* @param exportName Export name. Default is the "default" export.
|
* @param exportName Export name. Default is the "default" export.
|
||||||
*/
|
*/
|
||||||
|
@ -342,6 +346,8 @@ export class CommandsLoader {
|
||||||
/**
|
/**
|
||||||
* Load commands from a Directory.
|
* Load commands from a Directory.
|
||||||
*
|
*
|
||||||
|
* NOTE: Relative paths resolve from cwd
|
||||||
|
*
|
||||||
* @param path Path of the directory.
|
* @param path Path of the directory.
|
||||||
* @param options Options to configure loading.
|
* @param options Options to configure loading.
|
||||||
*/
|
*/
|
||||||
|
@ -486,12 +492,16 @@ export class CommandsManager {
|
||||||
|
|
||||||
/** Add a Command */
|
/** Add a Command */
|
||||||
add(cmd: Command | typeof Command): boolean {
|
add(cmd: Command | typeof Command): boolean {
|
||||||
// eslint-disable-next-line new-cap
|
if (!(cmd instanceof Command)) {
|
||||||
if (!(cmd instanceof Command)) cmd = new cmd()
|
const CmdClass = cmd
|
||||||
|
cmd = new CmdClass()
|
||||||
|
Object.assign(cmd, CmdClass.meta ?? {})
|
||||||
|
}
|
||||||
if (this.exists(cmd, cmd.extension?.subPrefix))
|
if (this.exists(cmd, cmd.extension?.subPrefix))
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to add Command '${cmd.toString()}' with name/alias already exists.`
|
`Failed to add Command '${cmd.toString()}' with name/alias already exists.`
|
||||||
)
|
)
|
||||||
|
if (cmd.name === '') throw new Error('Command has no name')
|
||||||
this.list.set(
|
this.list.set(
|
||||||
`${cmd.name}-${
|
`${cmd.name}-${
|
||||||
this.list.filter((e) =>
|
this.list.filter((e) =>
|
|
@ -1,7 +1,7 @@
|
||||||
import { ClientEvents } from '../../mod.ts'
|
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { Command } from './command.ts'
|
import { Command } from './command.ts'
|
||||||
import { CommandClient } from './commandClient.ts'
|
import { CommandClient } from './client.ts'
|
||||||
|
import type { ClientEvents } from '../gateway/handlers/mod.ts'
|
||||||
|
|
||||||
export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any
|
export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any
|
||||||
|
|
3
src/commands/mod.ts
Normal file
3
src/commands/mod.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './client.ts'
|
||||||
|
export * from './command.ts'
|
||||||
|
export * from './extension.ts'
|
|
@ -1,9 +0,0 @@
|
||||||
export const DISCORD_API_URL: string = 'https://discord.com/api'
|
|
||||||
|
|
||||||
export const DISCORD_GATEWAY_URL: string = 'wss://gateway.discord.gg'
|
|
||||||
|
|
||||||
export const DISCORD_CDN_URL: string = 'https://cdn.discordapp.com'
|
|
||||||
|
|
||||||
export const DISCORD_API_VERSION: number = 8
|
|
||||||
|
|
||||||
export const DISCORD_VOICE_VERSION: number = 4
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SlashCommand } from '../../models/slashClient.ts'
|
import { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const applicationCommandCreate: GatewayEventHandler = async (
|
export const applicationCommandCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SlashCommand } from '../../models/slashClient.ts'
|
import { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const applicationCommandDelete: GatewayEventHandler = async (
|
export const applicationCommandDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SlashCommand } from '../../models/slashClient.ts'
|
import { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
import { ApplicationCommandPayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const applicationCommandUpdate: GatewayEventHandler = async (
|
export const applicationCommandUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import getChannelByType from '../../utils/getChannelByType.ts'
|
import getChannelByType from '../../utils/channel.ts'
|
||||||
import { ChannelPayload, GuildChannelPayload } from '../../types/channel.ts'
|
import type {
|
||||||
import { Guild } from '../../structures/guild.ts'
|
ChannelPayload,
|
||||||
|
GuildChannelPayload
|
||||||
|
} from '../../types/channel.ts'
|
||||||
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
|
|
||||||
export const channelCreate: GatewayEventHandler = async (
|
export const channelCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { ChannelPayload } from '../../types/channel.ts'
|
import type { ChannelPayload } from '../../types/channel.ts'
|
||||||
|
|
||||||
export const channelDelete: GatewayEventHandler = async (
|
export const channelDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { ChannelPinsUpdatePayload } from '../../types/gateway.ts'
|
import type { ChannelPinsUpdatePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
export const channelPinsUpdate: GatewayEventHandler = async (
|
export const channelPinsUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Channel } from '../../structures/channel.ts'
|
import type { Channel } from '../../structures/channel.ts'
|
||||||
import { ChannelPayload } from '../../types/channel.ts'
|
import type { ChannelPayload } from '../../types/channel.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const channelUpdate: GatewayEventHandler = async (
|
export const channelUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildBanAddPayload } from '../../types/gateway.ts'
|
import { GuildBanAddPayload } from '../../types/gateway.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildBanRemovePayload } from '../../types/gateway.ts'
|
import { GuildBanRemovePayload } from '../../types/gateway.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
import { GuildChannelPayload } from '../../types/channel.ts'
|
import { GuildChannelPayload } from '../../types/channel.ts'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const guildDelete: GatewayEventHandler = async (
|
export const guildDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Emoji } from '../../structures/emoji.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { EmojiPayload } from '../../types/emoji.ts'
|
import { EmojiPayload } from '../../types/emoji.ts'
|
||||||
import { GuildEmojiUpdatePayload } from '../../types/gateway.ts'
|
import { GuildEmojiUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const guildEmojiUpdate: GatewayEventHandler = async (
|
export const guildEmojiUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildIntegrationsUpdatePayload } from '../../types/gateway.ts'
|
import { GuildIntegrationsUpdatePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildMemberAddPayload } from '../../types/gateway.ts'
|
import { GuildMemberAddPayload } from '../../types/gateway.ts'
|
||||||
import { Member } from '../../structures/member.ts'
|
import { Member } from '../../structures/member.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildMemberRemovePayload } from '../../types/gateway.ts'
|
import { GuildMemberRemovePayload } from '../../types/gateway.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildMemberUpdatePayload } from '../../types/gateway.ts'
|
import { GuildMemberUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { MemberPayload } from '../../types/guild.ts'
|
import { MemberPayload } from '../../types/guild.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildMemberChunkPayload } from '../../types/gateway.ts'
|
import { GuildMemberChunkPayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildRoleCreatePayload } from '../../types/gateway.ts'
|
import { GuildRoleCreatePayload } from '../../types/gateway.ts'
|
||||||
import { Role } from '../../structures/role.ts'
|
import { Role } from '../../structures/role.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildRoleDeletePayload } from '../../types/gateway.ts'
|
import { GuildRoleDeletePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildRoleUpdatePayload } from '../../types/gateway.ts'
|
import { GuildRoleUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Role } from '../../structures/role.ts'
|
import { Role } from '../../structures/role.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,110 @@
|
||||||
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { Member } from '../../structures/member.ts'
|
import { Member } from '../../structures/member.ts'
|
||||||
import { Interaction } from '../../structures/slash.ts'
|
import {
|
||||||
|
Interaction,
|
||||||
|
InteractionApplicationCommandResolved,
|
||||||
|
InteractionChannel
|
||||||
|
} from '../../structures/slash.ts'
|
||||||
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
import { InteractionPayload } from '../../types/slash.ts'
|
import { InteractionPayload } from '../../types/slash.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { UserPayload } from '../../types/user.ts'
|
||||||
|
import { Permissions } from '../../utils/permissions.ts'
|
||||||
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
import { User } from '../../structures/user.ts'
|
||||||
|
import { Role } from '../../structures/role.ts'
|
||||||
|
|
||||||
export const interactionCreate: GatewayEventHandler = async (
|
export const interactionCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: InteractionPayload
|
d: InteractionPayload
|
||||||
) => {
|
) => {
|
||||||
const guild = await gateway.client.guilds.get(d.guild_id)
|
// NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction.
|
||||||
if (guild === undefined) return
|
// This case can be seen in future proofing Interactions, and one he mentioned was
|
||||||
|
// that bots will be able to add custom context menus. In that case, Interaction will not have it.
|
||||||
|
// Ref: https://github.com/discord/discord-api-docs/pull/2568/files#r569025697
|
||||||
|
if (d.channel_id === undefined) return
|
||||||
|
|
||||||
await guild.members.set(d.member.user.id, d.member)
|
const guild =
|
||||||
const member = ((await guild.members.get(
|
d.guild_id === undefined
|
||||||
d.member.user.id
|
? undefined
|
||||||
)) as unknown) as Member
|
: await gateway.client.guilds.get(d.guild_id)
|
||||||
|
|
||||||
|
if (d.member !== undefined)
|
||||||
|
await guild?.members.set(d.member.user.id, d.member)
|
||||||
|
const member =
|
||||||
|
d.member !== undefined
|
||||||
|
? (((await guild?.members.get(d.member.user.id)) as unknown) as Member)
|
||||||
|
: undefined
|
||||||
|
if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user)
|
||||||
|
const dmUser =
|
||||||
|
d.user !== undefined ? await gateway.client.users.get(d.user.id) : undefined
|
||||||
|
|
||||||
|
const user = member !== undefined ? member.user : dmUser
|
||||||
|
if (user === undefined) return
|
||||||
|
|
||||||
const channel =
|
const channel =
|
||||||
(await gateway.client.channels.get<GuildTextBasedChannel>(d.channel_id)) ??
|
(await gateway.client.channels.get<GuildTextBasedChannel>(d.channel_id)) ??
|
||||||
(await gateway.client.channels.fetch<GuildTextBasedChannel>(d.channel_id))
|
(await gateway.client.channels.fetch<GuildTextBasedChannel>(d.channel_id))
|
||||||
|
|
||||||
|
const resolved: InteractionApplicationCommandResolved = {
|
||||||
|
users: {},
|
||||||
|
channels: {},
|
||||||
|
members: {},
|
||||||
|
roles: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.data?.resolved !== undefined) {
|
||||||
|
for (const [id, data] of Object.entries(d.data.resolved.users ?? {})) {
|
||||||
|
await gateway.client.users.set(id, data)
|
||||||
|
resolved.users[id] = ((await gateway.client.users.get(
|
||||||
|
id
|
||||||
|
)) as unknown) as User
|
||||||
|
if (resolved.members[id] !== undefined)
|
||||||
|
resolved.users[id].member = resolved.members[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, data] of Object.entries(d.data.resolved.members ?? {})) {
|
||||||
|
const roles = await guild?.roles.array()
|
||||||
|
let permissions = new Permissions(Permissions.DEFAULT)
|
||||||
|
if (roles !== undefined) {
|
||||||
|
const mRoles = roles.filter(
|
||||||
|
(r) => (data?.roles?.includes(r.id) as boolean) || r.id === guild?.id
|
||||||
|
)
|
||||||
|
permissions = new Permissions(mRoles.map((r) => r.permissions))
|
||||||
|
}
|
||||||
|
data.user = (d.data.resolved.users?.[id] as unknown) as UserPayload
|
||||||
|
resolved.members[id] = new Member(
|
||||||
|
gateway.client,
|
||||||
|
data,
|
||||||
|
resolved.users[id],
|
||||||
|
guild as Guild,
|
||||||
|
permissions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, data] of Object.entries(d.data.resolved.roles ?? {})) {
|
||||||
|
if (guild !== undefined) {
|
||||||
|
await guild.roles.set(id, data)
|
||||||
|
resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role
|
||||||
|
} else {
|
||||||
|
resolved.roles[id] = new Role(
|
||||||
|
gateway.client,
|
||||||
|
data,
|
||||||
|
(guild as unknown) as Guild
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, data] of Object.entries(d.data.resolved.channels ?? {})) {
|
||||||
|
resolved.channels[id] = new InteractionChannel(gateway.client, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const interaction = new Interaction(gateway.client, d, {
|
const interaction = new Interaction(gateway.client, d, {
|
||||||
member,
|
member,
|
||||||
guild,
|
guild,
|
||||||
channel
|
channel,
|
||||||
|
user,
|
||||||
|
resolved
|
||||||
})
|
})
|
||||||
gateway.client.emit('interactionCreate', interaction)
|
gateway.client.emit('interactionCreate', interaction)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { InviteCreatePayload } from '../../types/gateway.ts'
|
import { InviteCreatePayload } from '../../types/gateway.ts'
|
||||||
import { ChannelPayload } from '../../types/channel.ts'
|
import { ChannelPayload } from '../../types/channel.ts'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { InviteDeletePayload } from '../../types/gateway.ts'
|
import { InviteDeletePayload } from '../../types/gateway.ts'
|
||||||
import { PartialInvitePayload } from '../../types/invite.ts'
|
import { PartialInvitePayload } from '../../types/invite.ts'
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Message } from '../../structures/message.ts'
|
import { Message } from '../../structures/message.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { MessagePayload } from '../../types/channel.ts'
|
import type { MessagePayload } from '../../types/channel.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageCreate: GatewayEventHandler = async (
|
export const messageCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { MessageDeletePayload } from '../../types/gateway.ts'
|
import type { MessageDeletePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageDelete: GatewayEventHandler = async (
|
export const messageDelete: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Message } from '../../structures/message.ts'
|
import type { Message } from '../../structures/message.ts'
|
||||||
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
import { MessageDeleteBulkPayload } from '../../types/gateway.ts'
|
import type { MessageDeleteBulkPayload } from '../../types/gateway.ts'
|
||||||
import { Collection } from '../../utils/collection.ts'
|
import { Collection } from '../../utils/collection.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageDeleteBulk: GatewayEventHandler = async (
|
export const messageDeleteBulk: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionAddPayload } from '../../types/gateway.ts'
|
import type { MessageReactionAddPayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { MessageReaction } from '../../structures/messageReaction.ts'
|
import type { MessageReaction } from '../../structures/messageReaction.ts'
|
||||||
import { UserPayload } from '../../types/user.ts'
|
import type { UserPayload } from '../../types/user.ts'
|
||||||
|
|
||||||
export const messageReactionAdd: GatewayEventHandler = async (
|
export const messageReactionAdd: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionRemovePayload } from '../../types/gateway.ts'
|
import type { MessageReactionRemovePayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const messageReactionRemove: GatewayEventHandler = async (
|
export const messageReactionRemove: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionRemoveAllPayload } from '../../types/gateway.ts'
|
import type { MessageReactionRemoveAllPayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const messageReactionRemoveAll: GatewayEventHandler = async (
|
export const messageReactionRemoveAll: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts'
|
import type { MessageReactionRemoveEmojiPayload } from '../../types/gateway.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
|
||||||
export const messageReactionRemoveEmoji: GatewayEventHandler = async (
|
export const messageReactionRemoveEmoji: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Message } from '../../structures/message.ts'
|
import type { Message } from '../../structures/message.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { MessagePayload } from '../../types/channel.ts'
|
import type { MessagePayload } from '../../types/channel.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const messageUpdate: GatewayEventHandler = async (
|
export const messageUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { GatewayEventHandler } from '../index.ts'
|
import type { GatewayEventHandler } from '../mod.ts'
|
||||||
import {
|
import type {
|
||||||
GatewayEvents,
|
GatewayEvents,
|
||||||
MessageDeletePayload,
|
MessageDeletePayload,
|
||||||
TypingStartGuildData
|
TypingStartGuildData
|
||||||
|
@ -31,18 +31,18 @@ import { webhooksUpdate } from './webhooksUpdate.ts'
|
||||||
import { messageDeleteBulk } from './messageDeleteBulk.ts'
|
import { messageDeleteBulk } from './messageDeleteBulk.ts'
|
||||||
import { userUpdate } from './userUpdate.ts'
|
import { userUpdate } from './userUpdate.ts'
|
||||||
import { typingStart } from './typingStart.ts'
|
import { typingStart } from './typingStart.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import type { User } from '../../structures/user.ts'
|
||||||
import { Emoji } from '../../structures/emoji.ts'
|
import type { Emoji } from '../../structures/emoji.ts'
|
||||||
import { Member } from '../../structures/member.ts'
|
import type { Member } from '../../structures/member.ts'
|
||||||
import { Role } from '../../structures/role.ts'
|
import type { Role } from '../../structures/role.ts'
|
||||||
import { Message } from '../../structures/message.ts'
|
import type { Message } from '../../structures/message.ts'
|
||||||
import { Collection } from '../../utils/collection.ts'
|
import type { Collection } from '../../utils/collection.ts'
|
||||||
import { voiceServerUpdate } from './voiceServerUpdate.ts'
|
import { voiceServerUpdate } from './voiceServerUpdate.ts'
|
||||||
import { voiceStateUpdate } from './voiceStateUpdate.ts'
|
import { voiceStateUpdate } from './voiceStateUpdate.ts'
|
||||||
import { VoiceState } from '../../structures/voiceState.ts'
|
import type { VoiceState } from '../../structures/voiceState.ts'
|
||||||
import { messageReactionAdd } from './messageReactionAdd.ts'
|
import { messageReactionAdd } from './messageReactionAdd.ts'
|
||||||
import { messageReactionRemove } from './messageReactionRemove.ts'
|
import { messageReactionRemove } from './messageReactionRemove.ts'
|
||||||
import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts'
|
import { messageReactionRemoveAll } from './messageReactionRemoveAll.ts'
|
||||||
|
@ -51,23 +51,23 @@ import { guildMembersChunk } from './guildMembersChunk.ts'
|
||||||
import { presenceUpdate } from './presenceUpdate.ts'
|
import { presenceUpdate } from './presenceUpdate.ts'
|
||||||
import { inviteCreate } from './inviteCreate.ts'
|
import { inviteCreate } from './inviteCreate.ts'
|
||||||
import { inviteDelete } from './inviteDelete.ts'
|
import { inviteDelete } from './inviteDelete.ts'
|
||||||
import { MessageReaction } from '../../structures/messageReaction.ts'
|
import type { MessageReaction } from '../../structures/messageReaction.ts'
|
||||||
import { Invite } from '../../structures/invite.ts'
|
import type { Invite } from '../../structures/invite.ts'
|
||||||
import { Presence } from '../../structures/presence.ts'
|
import type { Presence } from '../../structures/presence.ts'
|
||||||
import {
|
import type {
|
||||||
EveryChannelTypes,
|
EveryChannelTypes,
|
||||||
EveryTextChannelTypes
|
EveryTextChannelTypes
|
||||||
} from '../../utils/getChannelByType.ts'
|
} from '../../utils/channel.ts'
|
||||||
import { interactionCreate } from './interactionCreate.ts'
|
import { interactionCreate } from './interactionCreate.ts'
|
||||||
import { Interaction } from '../../structures/slash.ts'
|
import type { Interaction } from '../../structures/slash.ts'
|
||||||
import { CommandContext } from '../../models/command.ts'
|
import type { CommandContext } from '../../commands/command.ts'
|
||||||
import { RequestMethods } from '../../models/rest.ts'
|
import type { RequestMethods } from '../../rest/types.ts'
|
||||||
import { PartialInvitePayload } from '../../types/invite.ts'
|
import type { PartialInvitePayload } from '../../types/invite.ts'
|
||||||
import { GuildChannels } from '../../types/guild.ts'
|
import type { GuildChannels } from '../../types/guild.ts'
|
||||||
import { applicationCommandCreate } from './applicationCommandCreate.ts'
|
import { applicationCommandCreate } from './applicationCommandCreate.ts'
|
||||||
import { applicationCommandDelete } from './applicationCommandDelete.ts'
|
import { applicationCommandDelete } from './applicationCommandDelete.ts'
|
||||||
import { applicationCommandUpdate } from './applicationCommandUpdate.ts'
|
import { applicationCommandUpdate } from './applicationCommandUpdate.ts'
|
||||||
import { SlashCommand } from '../../models/slashClient.ts'
|
import type { SlashCommand } from '../../interactions/slashCommand.ts'
|
||||||
|
|
||||||
export const gatewayHandlers: {
|
export const gatewayHandlers: {
|
||||||
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
||||||
|
@ -393,7 +393,15 @@ export type ClientEvents = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
guildMembersChunked: [guild: Guild, chunks: number]
|
guildMembersChunked: [guild: Guild, chunks: number]
|
||||||
rateLimit: [data: { method: RequestMethods; url: string; body: any }]
|
rateLimit: [
|
||||||
|
data: {
|
||||||
|
method: RequestMethods
|
||||||
|
path: string
|
||||||
|
global: boolean
|
||||||
|
timeout: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
]
|
||||||
inviteDeleteUncached: [invite: PartialInvitePayload]
|
inviteDeleteUncached: [invite: PartialInvitePayload]
|
||||||
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
|
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
|
||||||
userUpdateUncached: [user: User]
|
userUpdateUncached: [user: User]
|
||||||
|
@ -414,4 +422,5 @@ export type ClientEvents = {
|
||||||
commandMissingArgs: [ctx: CommandContext]
|
commandMissingArgs: [ctx: CommandContext]
|
||||||
commandUsed: [ctx: CommandContext]
|
commandUsed: [ctx: CommandContext]
|
||||||
commandError: [ctx: CommandContext, err: Error]
|
commandError: [ctx: CommandContext, err: Error]
|
||||||
|
gatewayError: [err: ErrorEvent, shards: [number, number]]
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { PresenceUpdatePayload } from '../../types/gateway.ts'
|
import type { PresenceUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const presenceUpdate: GatewayEventHandler = async (
|
export const presenceUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { Ready } from '../../types/gateway.ts'
|
import type { Ready } from '../../types/gateway.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import type { GuildPayload } from '../../types/guild.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const ready: GatewayEventHandler = async (
|
export const ready: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const reconnect: GatewayEventHandler = async (
|
export const reconnect: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { CLIENT_USER } from '../../types/endpoint.ts'
|
import { CLIENT_USER } from '../../types/endpoint.ts'
|
||||||
import { Resume } from '../../types/gateway.ts'
|
import type { Resume } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const resume: GatewayEventHandler = async (
|
export const resume: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Member } from '../../structures/member.ts'
|
import { Member } from '../../structures/member.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import type { TextChannel } from '../../structures/textChannel.ts'
|
||||||
import { TypingStartPayload } from '../../types/gateway.ts'
|
import type { TypingStartPayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
// TODO: Do we need to add uncached events here?
|
// TODO: Do we need to add uncached events here?
|
||||||
export const typingStart: GatewayEventHandler = async (
|
export const typingStart: GatewayEventHandler = async (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { User } from '../../structures/user.ts'
|
import type { User } from '../../structures/user.ts'
|
||||||
import { UserPayload } from '../../types/user.ts'
|
import type { UserPayload } from '../../types/user.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const userUpdate: GatewayEventHandler = async (
|
export const userUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { VoiceServerUpdatePayload } from '../../types/gateway.ts'
|
import type { VoiceServerUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const voiceServerUpdate: GatewayEventHandler = async (
|
export const voiceServerUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { VoiceState } from '../../structures/voiceState.ts'
|
import type { VoiceState } from '../../structures/voiceState.ts'
|
||||||
import { MemberPayload } from '../../types/guild.ts'
|
import type { MemberPayload } from '../../types/guild.ts'
|
||||||
import { VoiceStatePayload } from '../../types/voice.ts'
|
import type { VoiceStatePayload } from '../../types/voice.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
|
|
||||||
export const voiceStateUpdate: GatewayEventHandler = async (
|
export const voiceStateUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import type { Gateway, GatewayEventHandler } from '../mod.ts'
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import type { Guild } from '../../structures/guild.ts'
|
||||||
import { WebhooksUpdatePayload } from '../../types/gateway.ts'
|
import type { WebhooksUpdatePayload } from '../../types/gateway.ts'
|
||||||
import { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
import type { GuildTextBasedChannel } from '../../structures/guildTextChannel.ts'
|
||||||
|
|
||||||
export const webhooksUpdate: GatewayEventHandler = async (
|
export const webhooksUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import { unzlib } from '../../deps.ts'
|
import { unzlib } from '../../deps.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import {
|
|
||||||
DISCORD_GATEWAY_URL,
|
|
||||||
DISCORD_API_VERSION
|
|
||||||
} from '../consts/urlsAndVersions.ts'
|
|
||||||
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
||||||
import {
|
import {
|
||||||
GatewayOpcodes,
|
GatewayOpcodes,
|
||||||
GatewayIntents,
|
|
||||||
GatewayCloseCodes,
|
GatewayCloseCodes,
|
||||||
IdentityPayload,
|
IdentityPayload,
|
||||||
StatusUpdatePayload,
|
StatusUpdatePayload,
|
||||||
GatewayEvents
|
GatewayEvents
|
||||||
} from '../types/gateway.ts'
|
} from '../types/gateway.ts'
|
||||||
import { gatewayHandlers } from './handlers/index.ts'
|
import { gatewayHandlers } from './handlers/mod.ts'
|
||||||
import { GatewayCache } from '../managers/gatewayCache.ts'
|
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 type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { HarmonyEventEmitter } from '../utils/events.ts'
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import { decodeText } from '../utils/encoding.ts'
|
||||||
|
import { Constants } from '../types/constants.ts'
|
||||||
|
|
||||||
export interface RequestMembersOptions {
|
export interface RequestMembersOptions {
|
||||||
limit?: number
|
limit?: number
|
||||||
|
@ -57,8 +54,6 @@ export type GatewayTypedEvents = {
|
||||||
*/
|
*/
|
||||||
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
websocket?: WebSocket
|
websocket?: WebSocket
|
||||||
token?: string
|
|
||||||
intents?: GatewayIntents[]
|
|
||||||
connected = false
|
connected = false
|
||||||
initialized = false
|
initialized = false
|
||||||
heartbeatInterval = 0
|
heartbeatInterval = 0
|
||||||
|
@ -71,6 +66,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
cache: GatewayCache
|
cache: GatewayCache
|
||||||
private timedIdentify: number | null = null
|
private timedIdentify: number | null = null
|
||||||
shards?: number[]
|
shards?: number[]
|
||||||
|
ping: number = 0
|
||||||
|
|
||||||
constructor(client: Client, shards?: number[]) {
|
constructor(client: Client, shards?: number[]) {
|
||||||
super()
|
super()
|
||||||
|
@ -92,7 +88,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
}
|
}
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
data = unzlib(data)
|
data = unzlib(data)
|
||||||
data = new TextDecoder('utf-8').decode(data)
|
data = decodeText(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
|
const { op, d, s, t }: GatewayResponse = JSON.parse(data)
|
||||||
|
@ -120,11 +116,9 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
|
|
||||||
case GatewayOpcodes.HEARTBEAT_ACK:
|
case GatewayOpcodes.HEARTBEAT_ACK:
|
||||||
this.heartbeatServerResponded = true
|
this.heartbeatServerResponded = true
|
||||||
this.client.ping = Date.now() - this.lastPingTimestamp
|
this.ping = Date.now() - this.lastPingTimestamp
|
||||||
this.emit('ping', this.client.ping)
|
this.emit('ping', this.ping)
|
||||||
this.debug(
|
this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.ping}ms`)
|
||||||
`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case GatewayOpcodes.INVALID_SESSION:
|
case GatewayOpcodes.INVALID_SESSION:
|
||||||
|
@ -157,7 +151,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
|
|
||||||
const handler = gatewayHandlers[t]
|
const handler = gatewayHandlers[t]
|
||||||
|
|
||||||
if (handler !== undefined) {
|
if (handler !== undefined && d !== null) {
|
||||||
handler(this, d)
|
handler(this, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,8 +171,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
}
|
}
|
||||||
case GatewayOpcodes.RECONNECT: {
|
case GatewayOpcodes.RECONNECT: {
|
||||||
this.emit('reconnectRequired')
|
this.emit('reconnectRequired')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
this.debug('Received OpCode RECONNECT')
|
||||||
this.reconnect()
|
await this.reconnect()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -194,8 +188,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case GatewayCloseCodes.UNKNOWN_ERROR:
|
case GatewayCloseCodes.UNKNOWN_ERROR:
|
||||||
this.debug('API has encountered Unknown Error. Reconnecting...')
|
this.debug('API has encountered Unknown Error. Reconnecting...')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect()
|
||||||
this.reconnect()
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.UNKNOWN_OPCODE:
|
case GatewayCloseCodes.UNKNOWN_OPCODE:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -209,20 +202,17 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
throw new Error('Invalid Token provided!')
|
throw new Error('Invalid Token provided!')
|
||||||
case GatewayCloseCodes.INVALID_SEQ:
|
case GatewayCloseCodes.INVALID_SEQ:
|
||||||
this.debug('Invalid Seq was sent. Reconnecting.')
|
this.debug('Invalid Seq was sent. Reconnecting.')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect()
|
||||||
this.reconnect()
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.RATE_LIMITED:
|
case GatewayCloseCodes.RATE_LIMITED:
|
||||||
throw new Error("You're ratelimited. Calm down.")
|
throw new Error("You're ratelimited. Calm down.")
|
||||||
case GatewayCloseCodes.SESSION_TIMED_OUT:
|
case GatewayCloseCodes.SESSION_TIMED_OUT:
|
||||||
this.debug('Session Timeout. Reconnecting.')
|
this.debug('Session Timeout. Reconnecting.')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect(true)
|
||||||
this.reconnect(true)
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.INVALID_SHARD:
|
case GatewayCloseCodes.INVALID_SHARD:
|
||||||
this.debug('Invalid Shard was sent. Reconnecting.')
|
this.debug('Invalid Shard was sent. Reconnecting.')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.reconnect()
|
||||||
this.reconnect()
|
|
||||||
break
|
break
|
||||||
case GatewayCloseCodes.SHARDING_REQUIRED:
|
case GatewayCloseCodes.SHARDING_REQUIRED:
|
||||||
throw new Error("Couldn't connect. Sharding is required!")
|
throw new Error("Couldn't connect. Sharding is required!")
|
||||||
|
@ -260,6 +250,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
error.name = 'ErrorEvent'
|
error.name = 'ErrorEvent'
|
||||||
console.log(error)
|
console.log(error)
|
||||||
this.emit('error', error, event)
|
this.emit('error', error, event)
|
||||||
|
this.client.emit('gatewayError', event, this.shards)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enqueueIdentify(forceNew?: boolean): void {
|
private enqueueIdentify(forceNew?: boolean): void {
|
||||||
|
@ -269,25 +260,11 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
|
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
|
||||||
if (typeof this.token !== 'string') throw new Error('Token not specified')
|
if (typeof this.client.token !== 'string')
|
||||||
if (typeof this.intents !== 'object')
|
throw new Error('Token not specified')
|
||||||
|
if (typeof this.client.intents !== 'object')
|
||||||
throw new Error('Intents not specified')
|
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(`Reset After: ${info.session_start_limit.reset_after}ms`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceNewSession === undefined || !forceNewSession) {
|
if (forceNewSession === undefined || !forceNewSession) {
|
||||||
const sessionIDCached = await this.cache.get(
|
const sessionIDCached = await this.cache.get(
|
||||||
`session_id_${this.shards?.join('-') ?? '0'}`
|
`session_id_${this.shards?.join('-') ?? '0'}`
|
||||||
|
@ -300,7 +277,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: IdentityPayload = {
|
const payload: IdentityPayload = {
|
||||||
token: this.token,
|
token: this.client.token,
|
||||||
properties: {
|
properties: {
|
||||||
$os: this.client.clientProperties.os ?? Deno.build.os,
|
$os: this.client.clientProperties.os ?? Deno.build.os,
|
||||||
$browser: this.client.clientProperties.browser ?? 'harmony',
|
$browser: this.client.clientProperties.browser ?? 'harmony',
|
||||||
|
@ -311,7 +288,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
this.shards === undefined
|
this.shards === undefined
|
||||||
? [0, 1]
|
? [0, 1]
|
||||||
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
|
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
|
||||||
intents: this.intents.reduce(
|
intents: this.client.intents.reduce(
|
||||||
(previous, current) => previous | current,
|
(previous, current) => previous | current,
|
||||||
0
|
0
|
||||||
),
|
),
|
||||||
|
@ -327,9 +304,8 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendResume(): Promise<void> {
|
private async sendResume(): Promise<void> {
|
||||||
if (typeof this.token !== 'string') throw new Error('Token not specified')
|
if (typeof this.client.token !== 'string')
|
||||||
if (typeof this.intents !== 'object')
|
throw new Error('Token not specified')
|
||||||
throw new Error('Intents not specified')
|
|
||||||
|
|
||||||
if (this.sessionID === undefined) {
|
if (this.sessionID === undefined) {
|
||||||
this.sessionID = await this.cache.get(
|
this.sessionID = await this.cache.get(
|
||||||
|
@ -348,7 +324,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
const resumePayload = {
|
const resumePayload = {
|
||||||
op: GatewayOpcodes.RESUME,
|
op: GatewayOpcodes.RESUME,
|
||||||
d: {
|
d: {
|
||||||
token: this.token,
|
token: this.client.token,
|
||||||
session_id: this.sessionID,
|
session_id: this.sessionID,
|
||||||
seq: this.sequenceID ?? null
|
seq: this.sequenceID ?? null
|
||||||
}
|
}
|
||||||
|
@ -393,8 +369,18 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
: typeof channel === 'string'
|
: typeof channel === 'string'
|
||||||
? channel
|
? channel
|
||||||
: channel?.id,
|
: channel?.id,
|
||||||
self_mute: voiceOptions.mute === undefined ? false : voiceOptions.mute,
|
self_mute:
|
||||||
self_deaf: voiceOptions.deaf === undefined ? false : voiceOptions.deaf
|
channel === undefined
|
||||||
|
? undefined
|
||||||
|
: voiceOptions.mute === undefined
|
||||||
|
? false
|
||||||
|
: voiceOptions.mute,
|
||||||
|
self_deaf:
|
||||||
|
channel === undefined
|
||||||
|
? undefined
|
||||||
|
: voiceOptions.deaf === undefined
|
||||||
|
? false
|
||||||
|
: voiceOptions.deaf
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -405,6 +391,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
|
|
||||||
async reconnect(forceNew?: boolean): Promise<void> {
|
async reconnect(forceNew?: boolean): Promise<void> {
|
||||||
this.emit('reconnecting')
|
this.emit('reconnecting')
|
||||||
|
this.debug('Reconnecting... (force new: ' + String(forceNew) + ')')
|
||||||
|
|
||||||
clearInterval(this.heartbeatIntervalID)
|
clearInterval(this.heartbeatIntervalID)
|
||||||
if (forceNew === true) {
|
if (forceNew === true) {
|
||||||
|
@ -421,7 +408,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
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
|
||||||
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
|
`${Constants.DISCORD_GATEWAY_URL}/?v=${Constants.DISCORD_API_VERSION}&encoding=json`,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
this.websocket.binaryType = 'arraybuffer'
|
this.websocket.binaryType = 'arraybuffer'
|
||||||
|
@ -432,6 +419,11 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
close(code: number = 1000, reason?: string): void {
|
close(code: number = 1000, reason?: string): void {
|
||||||
|
this.debug(
|
||||||
|
`Closing with code ${code}${
|
||||||
|
reason !== undefined && reason !== '' ? ` and reason ${reason}` : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
return this.websocket?.close(code, reason)
|
return this.websocket?.close(code, reason)
|
||||||
}
|
}
|
||||||
|
|
3
src/interactions/mod.ts
Normal file
3
src/interactions/mod.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './slashClient.ts'
|
||||||
|
export * from './slashModule.ts'
|
||||||
|
export * from './slashCommand.ts'
|
440
src/interactions/slashClient.ts
Normal file
440
src/interactions/slashClient.ts
Normal file
|
@ -0,0 +1,440 @@
|
||||||
|
import {
|
||||||
|
Interaction,
|
||||||
|
InteractionApplicationCommandResolved
|
||||||
|
} from '../structures/slash.ts'
|
||||||
|
import {
|
||||||
|
InteractionPayload,
|
||||||
|
InteractionResponsePayload,
|
||||||
|
InteractionType,
|
||||||
|
SlashCommandOptionType
|
||||||
|
} from '../types/slash.ts'
|
||||||
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import { RESTManager } from '../rest/mod.ts'
|
||||||
|
import { SlashModule } from './slashModule.ts'
|
||||||
|
import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts'
|
||||||
|
import { User } from '../structures/user.ts'
|
||||||
|
import { HarmonyEventEmitter } from '../utils/events.ts'
|
||||||
|
import { encodeText, decodeText } from '../utils/encoding.ts'
|
||||||
|
import { SlashCommandsManager } from './slashCommand.ts'
|
||||||
|
|
||||||
|
export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown
|
||||||
|
export interface SlashCommandHandler {
|
||||||
|
name: string
|
||||||
|
guild?: string
|
||||||
|
parent?: string
|
||||||
|
group?: string
|
||||||
|
handler: SlashCommandHandlerCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options for SlashClient */
|
||||||
|
export interface SlashOptions {
|
||||||
|
id?: string | (() => string)
|
||||||
|
client?: Client
|
||||||
|
enabled?: boolean
|
||||||
|
token?: string
|
||||||
|
rest?: RESTManager
|
||||||
|
publicKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type SlashClientEvents = {
|
||||||
|
interaction: [Interaction]
|
||||||
|
interactionError: [Error]
|
||||||
|
ping: []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slash Client represents an Interactions Client which can be used without Harmony Client. */
|
||||||
|
export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> {
|
||||||
|
id: string | (() => string)
|
||||||
|
client?: Client
|
||||||
|
token?: string
|
||||||
|
enabled: boolean = true
|
||||||
|
commands: SlashCommandsManager
|
||||||
|
handlers: SlashCommandHandler[] = []
|
||||||
|
rest: RESTManager
|
||||||
|
modules: SlashModule[] = []
|
||||||
|
publicKey?: string
|
||||||
|
|
||||||
|
_decoratedSlash?: Array<{
|
||||||
|
name: string
|
||||||
|
guild?: string
|
||||||
|
parent?: string
|
||||||
|
group?: string
|
||||||
|
handler: (interaction: Interaction) => any
|
||||||
|
}>
|
||||||
|
|
||||||
|
constructor(options: SlashOptions) {
|
||||||
|
super()
|
||||||
|
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.publicKey = options.publicKey
|
||||||
|
|
||||||
|
this.enabled = options.enabled ?? true
|
||||||
|
|
||||||
|
if (this.client?._decoratedSlash !== undefined) {
|
||||||
|
this.client._decoratedSlash.forEach((e) => {
|
||||||
|
e.handler = e.handler.bind(this.client)
|
||||||
|
this.handlers.push(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._decoratedSlash !== undefined) {
|
||||||
|
this._decoratedSlash.forEach((e) => {
|
||||||
|
e.handler = e.handler.bind(this.client)
|
||||||
|
this.handlers.push(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rest =
|
||||||
|
options.client === undefined
|
||||||
|
? options.rest === undefined
|
||||||
|
? new RESTManager({
|
||||||
|
token: this.token
|
||||||
|
})
|
||||||
|
: options.rest
|
||||||
|
: options.client.rest
|
||||||
|
|
||||||
|
this.client?.on(
|
||||||
|
'interactionCreate',
|
||||||
|
async (interaction) => await this._process(interaction)
|
||||||
|
)
|
||||||
|
|
||||||
|
this.commands = new SlashCommandsManager(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getID(): string {
|
||||||
|
return typeof this.id === 'string' ? this.id : this.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds a new Slash Command Handler */
|
||||||
|
handle(
|
||||||
|
cmd: string | SlashCommandHandler,
|
||||||
|
handler?: SlashCommandHandlerCallback
|
||||||
|
): SlashClient {
|
||||||
|
const handle = {
|
||||||
|
name: typeof cmd === 'string' ? cmd : cmd.name,
|
||||||
|
...(handler !== undefined ? { handler } : {}),
|
||||||
|
...(typeof cmd === 'string' ? {} : cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.handler === undefined)
|
||||||
|
throw new Error('Invalid usage. Handler function not provided')
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof handle.name === 'string' &&
|
||||||
|
handle.name.includes(' ') &&
|
||||||
|
handle.parent === undefined &&
|
||||||
|
handle.group === undefined
|
||||||
|
) {
|
||||||
|
const parts = handle.name.split(/ +/).filter((e) => e !== '')
|
||||||
|
if (parts.length > 3 || parts.length < 1)
|
||||||
|
throw new Error('Invalid command name')
|
||||||
|
const root = parts.shift() as string
|
||||||
|
const group = parts.length === 2 ? parts.shift() : undefined
|
||||||
|
const sub = parts.shift()
|
||||||
|
|
||||||
|
handle.name = sub ?? root
|
||||||
|
handle.group = group
|
||||||
|
handle.parent = sub === undefined ? undefined : root
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers.push(handle as any)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load a Slash Module */
|
||||||
|
loadModule(module: SlashModule): SlashClient {
|
||||||
|
this.modules.push(module)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all Handlers. Including Slash Modules */
|
||||||
|
getHandlers(): SlashCommandHandler[] {
|
||||||
|
let res = this.handlers
|
||||||
|
for (const mod of this.modules) {
|
||||||
|
if (mod === undefined) continue
|
||||||
|
res = [
|
||||||
|
...res,
|
||||||
|
...mod.commands.map((cmd) => {
|
||||||
|
cmd.handler = cmd.handler.bind(mod)
|
||||||
|
return cmd
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */
|
||||||
|
private _getCommand(i: Interaction): SlashCommandHandler | undefined {
|
||||||
|
return this.getHandlers().find((e) => {
|
||||||
|
const hasGroupOrParent = e.group !== undefined || e.parent !== undefined
|
||||||
|
const groupMatched =
|
||||||
|
e.group !== undefined && e.parent !== undefined
|
||||||
|
? i.options
|
||||||
|
.find(
|
||||||
|
(o) =>
|
||||||
|
o.name === e.group &&
|
||||||
|
o.type === SlashCommandOptionType.SUB_COMMAND_GROUP
|
||||||
|
)
|
||||||
|
?.options?.find((o) => o.name === e.name) !== undefined
|
||||||
|
: true
|
||||||
|
const subMatched =
|
||||||
|
e.group === undefined && e.parent !== undefined
|
||||||
|
? i.options.find(
|
||||||
|
(o) =>
|
||||||
|
o.name === e.name &&
|
||||||
|
o.type === SlashCommandOptionType.SUB_COMMAND
|
||||||
|
) !== undefined
|
||||||
|
: true
|
||||||
|
const nameMatched1 = e.name === i.name
|
||||||
|
const parentMatched = hasGroupOrParent ? e.parent === i.name : true
|
||||||
|
const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1
|
||||||
|
|
||||||
|
const matched = groupMatched && subMatched && nameMatched
|
||||||
|
return matched
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Process an incoming Interaction */
|
||||||
|
private async _process(interaction: Interaction): Promise<void> {
|
||||||
|
if (!this.enabled) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
interaction.type !== InteractionType.APPLICATION_COMMAND ||
|
||||||
|
interaction.data === undefined
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const cmd =
|
||||||
|
this._getCommand(interaction) ??
|
||||||
|
this.getHandlers().find((e) => e.name === '*')
|
||||||
|
if (cmd?.group !== undefined)
|
||||||
|
interaction.data.options = interaction.data.options[0].options ?? []
|
||||||
|
if (cmd?.parent !== undefined)
|
||||||
|
interaction.data.options = interaction.data.options[0].options ?? []
|
||||||
|
|
||||||
|
if (cmd === undefined) return
|
||||||
|
|
||||||
|
await this.emit('interaction', interaction)
|
||||||
|
try {
|
||||||
|
await cmd.handler(interaction)
|
||||||
|
} catch (e) {
|
||||||
|
await this.emit('interactionError', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify HTTP based Interaction */
|
||||||
|
async verifyKey(
|
||||||
|
rawBody: string | Uint8Array,
|
||||||
|
signature: string | Uint8Array,
|
||||||
|
timestamp: string | Uint8Array
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (this.publicKey === undefined)
|
||||||
|
throw new Error('Public Key is not present')
|
||||||
|
|
||||||
|
const fullBody = new Uint8Array([
|
||||||
|
...(typeof timestamp === 'string' ? encodeText(timestamp) : timestamp),
|
||||||
|
...(typeof rawBody === 'string' ? encodeText(rawBody) : rawBody)
|
||||||
|
])
|
||||||
|
|
||||||
|
return edverify(signature, fullBody, this.publicKey).catch(() => false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction. **Data present in Interaction returned by this method is very different from actual typings as there is no real `Client` behind the scenes to cache things.** */
|
||||||
|
async verifyServerRequest(req: {
|
||||||
|
headers: Headers
|
||||||
|
method: string
|
||||||
|
body: Deno.Reader | Uint8Array
|
||||||
|
respond: (options: {
|
||||||
|
status?: number
|
||||||
|
headers?: Headers
|
||||||
|
body?: any
|
||||||
|
}) => Promise<void>
|
||||||
|
}): Promise<false | Interaction> {
|
||||||
|
if (req.method.toLowerCase() !== 'post') return false
|
||||||
|
|
||||||
|
const signature = req.headers.get('x-signature-ed25519')
|
||||||
|
const timestamp = req.headers.get('x-signature-timestamp')
|
||||||
|
if (signature === null || timestamp === null) return false
|
||||||
|
|
||||||
|
const rawbody =
|
||||||
|
req.body instanceof Uint8Array ? req.body : await Deno.readAll(req.body)
|
||||||
|
const verify = await this.verifyKey(rawbody, signature, timestamp)
|
||||||
|
if (!verify) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: InteractionPayload = JSON.parse(decodeText(rawbody))
|
||||||
|
|
||||||
|
// TODO: Maybe fix all this hackery going on here?
|
||||||
|
const res = new Interaction(this as any, payload, {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
user: new User(this as any, (payload.member?.user ?? payload.user)!),
|
||||||
|
member: payload.member as any,
|
||||||
|
guild: payload.guild_id as any,
|
||||||
|
channel: payload.channel_id as any,
|
||||||
|
resolved: ((payload.data
|
||||||
|
?.resolved as unknown) as InteractionApplicationCommandResolved) ?? {
|
||||||
|
users: {},
|
||||||
|
members: {},
|
||||||
|
roles: {},
|
||||||
|
channels: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res._httpRespond = async (d: InteractionResponsePayload | FormData) =>
|
||||||
|
await req.respond({
|
||||||
|
status: 200,
|
||||||
|
headers: new Headers({
|
||||||
|
'content-type':
|
||||||
|
d instanceof FormData ? 'multipart/form-data' : 'application/json'
|
||||||
|
}),
|
||||||
|
body: d instanceof FormData ? d : JSON.stringify(d)
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify FetchEvent (for Service Worker usage) and return Interaction if valid */
|
||||||
|
async verifyFetchEvent({
|
||||||
|
request: req,
|
||||||
|
respondWith
|
||||||
|
}: {
|
||||||
|
respondWith: CallableFunction
|
||||||
|
request: Request
|
||||||
|
}): Promise<false | Interaction> {
|
||||||
|
if (req.bodyUsed === true) throw new Error('Request Body already used')
|
||||||
|
if (req.body === null) return false
|
||||||
|
const body = (await req.body.getReader().read()).value
|
||||||
|
if (body === undefined) return false
|
||||||
|
|
||||||
|
return await this.verifyServerRequest({
|
||||||
|
headers: req.headers,
|
||||||
|
body,
|
||||||
|
method: req.method,
|
||||||
|
respond: async (options) => {
|
||||||
|
await respondWith(
|
||||||
|
new Response(options.body, {
|
||||||
|
headers: options.headers,
|
||||||
|
status: options.status
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOpineRequest(req: any): Promise<boolean> {
|
||||||
|
const signature = req.headers.get('x-signature-ed25519')
|
||||||
|
const timestamp = req.headers.get('x-signature-timestamp')
|
||||||
|
const contentLength = req.headers.get('content-length')
|
||||||
|
|
||||||
|
if (signature === null || timestamp === null || contentLength === null)
|
||||||
|
return false
|
||||||
|
|
||||||
|
const body = new Uint8Array(parseInt(contentLength))
|
||||||
|
await req.body.read(body)
|
||||||
|
|
||||||
|
const verified = await this.verifyKey(body, signature, timestamp)
|
||||||
|
if (!verified) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Middleware to verify request in Opine framework. */
|
||||||
|
async verifyOpineMiddleware(
|
||||||
|
req: any,
|
||||||
|
res: any,
|
||||||
|
next: CallableFunction
|
||||||
|
): Promise<any> {
|
||||||
|
const verified = await this.verifyOpineRequest(req)
|
||||||
|
if (!verified) return res.setStatus(401).end()
|
||||||
|
|
||||||
|
await next()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: create verifyOakMiddleware too
|
||||||
|
/** Method to verify Request from Oak server "Context". */
|
||||||
|
async verifyOakRequest(ctx: any): Promise<any> {
|
||||||
|
const signature = ctx.request.headers.get('x-signature-ed25519')
|
||||||
|
const timestamp = ctx.request.headers.get('x-signature-timestamp')
|
||||||
|
const contentLength = ctx.request.headers.get('content-length')
|
||||||
|
|
||||||
|
if (
|
||||||
|
signature === null ||
|
||||||
|
timestamp === null ||
|
||||||
|
contentLength === null ||
|
||||||
|
ctx.request.hasBody !== true
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await ctx.request.body().value
|
||||||
|
|
||||||
|
const verified = await this.verifyKey(body, signature, timestamp)
|
||||||
|
if (!verified) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decorator to create a Slash Command handler */
|
||||||
|
export function slash(name?: string, guild?: string) {
|
||||||
|
return function (client: Client | SlashClient | SlashModule, prop: string) {
|
||||||
|
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
||||||
|
const item = (client as { [name: string]: any })[prop]
|
||||||
|
if (typeof item !== 'function') {
|
||||||
|
throw new Error('@slash decorator requires a function')
|
||||||
|
} else
|
||||||
|
client._decoratedSlash.push({
|
||||||
|
name: name ?? prop,
|
||||||
|
guild,
|
||||||
|
handler: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decorator to create a Sub-Slash Command handler */
|
||||||
|
export function subslash(parent: string, name?: string, guild?: string) {
|
||||||
|
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
||||||
|
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
||||||
|
const item = (client as { [name: string]: any })[prop]
|
||||||
|
if (typeof item !== 'function') {
|
||||||
|
throw new Error('@subslash decorator requires a function')
|
||||||
|
} else
|
||||||
|
client._decoratedSlash.push({
|
||||||
|
parent,
|
||||||
|
name: name ?? prop,
|
||||||
|
guild,
|
||||||
|
handler: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decorator to create a Grouped Slash Command handler */
|
||||||
|
export function groupslash(
|
||||||
|
parent: string,
|
||||||
|
group: string,
|
||||||
|
name?: string,
|
||||||
|
guild?: string
|
||||||
|
) {
|
||||||
|
return function (client: Client | SlashModule | SlashClient, prop: string) {
|
||||||
|
if (client._decoratedSlash === undefined) client._decoratedSlash = []
|
||||||
|
const item = (client as { [name: string]: any })[prop]
|
||||||
|
if (typeof item !== 'function') {
|
||||||
|
throw new Error('@groupslash decorator requires a function')
|
||||||
|
} else
|
||||||
|
client._decoratedSlash.push({
|
||||||
|
group,
|
||||||
|
parent,
|
||||||
|
name: name ?? prop,
|
||||||
|
guild,
|
||||||
|
handler: item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { RESTManager } from '../rest/manager.ts'
|
||||||
import { Interaction } from '../structures/slash.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import {
|
import {
|
||||||
InteractionType,
|
|
||||||
SlashCommandChoice,
|
SlashCommandChoice,
|
||||||
SlashCommandOption,
|
SlashCommandOption,
|
||||||
SlashCommandOptionType,
|
SlashCommandOptionType,
|
||||||
|
@ -9,11 +8,7 @@ import {
|
||||||
SlashCommandPayload
|
SlashCommandPayload
|
||||||
} 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 type { SlashClient, SlashCommandHandlerCallback } from './slashClient.ts'
|
||||||
import { RESTManager } from './rest.ts'
|
|
||||||
import { SlashModule } from './slashModule.ts'
|
|
||||||
import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts'
|
|
||||||
import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts'
|
|
||||||
|
|
||||||
export class SlashCommand {
|
export class SlashCommand {
|
||||||
slash: SlashCommandsManager
|
slash: SlashCommandsManager
|
||||||
|
@ -155,6 +150,7 @@ function buildOptionsArray(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Slash Command Builder */
|
||||||
export class SlashBuilder {
|
export class SlashBuilder {
|
||||||
data: SlashCommandPartial
|
data: SlashCommandPartial
|
||||||
|
|
||||||
|
@ -200,6 +196,7 @@ export class SlashBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */
|
||||||
export class SlashCommandsManager {
|
export class SlashCommandsManager {
|
||||||
slash: SlashClient
|
slash: SlashClient
|
||||||
rest: RESTManager
|
rest: RESTManager
|
||||||
|
@ -350,229 +347,3 @@ export class SlashCommandsManager {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SlashCommandHandlerCallback = (interaction: Interaction) => any
|
|
||||||
export interface SlashCommandHandler {
|
|
||||||
name: string
|
|
||||||
guild?: string
|
|
||||||
parent?: string
|
|
||||||
group?: string
|
|
||||||
handler: SlashCommandHandlerCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashOptions {
|
|
||||||
id?: string | (() => string)
|
|
||||||
client?: Client
|
|
||||||
enabled?: boolean
|
|
||||||
token?: string
|
|
||||||
rest?: RESTManager
|
|
||||||
publicKey?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SlashClient {
|
|
||||||
id: string | (() => string)
|
|
||||||
client?: Client
|
|
||||||
token?: string
|
|
||||||
enabled: boolean = true
|
|
||||||
commands: SlashCommandsManager
|
|
||||||
handlers: SlashCommandHandler[] = []
|
|
||||||
rest: RESTManager
|
|
||||||
modules: SlashModule[] = []
|
|
||||||
publicKey?: string
|
|
||||||
|
|
||||||
_decoratedSlash?: Array<{
|
|
||||||
name: string
|
|
||||||
guild?: string
|
|
||||||
parent?: string
|
|
||||||
group?: string
|
|
||||||
handler: (interaction: Interaction) => any
|
|
||||||
}>
|
|
||||||
|
|
||||||
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.publicKey = options.publicKey
|
|
||||||
|
|
||||||
this.enabled = options.enabled ?? true
|
|
||||||
|
|
||||||
if (this.client?._decoratedSlash !== undefined) {
|
|
||||||
this.client._decoratedSlash.forEach((e) => {
|
|
||||||
e.handler = e.handler.bind(this.client)
|
|
||||||
this.handlers.push(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._decoratedSlash !== undefined) {
|
|
||||||
this._decoratedSlash.forEach((e) => {
|
|
||||||
e.handler = e.handler.bind(this.client)
|
|
||||||
this.handlers.push(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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.commands = new SlashCommandsManager(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load a Slash Module */
|
|
||||||
loadModule(module: SlashModule): SlashClient {
|
|
||||||
this.modules.push(module)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get all Handlers. Including Slash Modules */
|
|
||||||
getHandlers(): SlashCommandHandler[] {
|
|
||||||
let res = this.handlers
|
|
||||||
for (const mod of this.modules) {
|
|
||||||
if (mod === undefined) continue
|
|
||||||
res = [
|
|
||||||
...res,
|
|
||||||
...mod.commands.map((cmd) => {
|
|
||||||
cmd.handler = cmd.handler.bind(mod)
|
|
||||||
return cmd
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */
|
|
||||||
private _getCommand(i: Interaction): SlashCommandHandler | undefined {
|
|
||||||
return this.getHandlers().find((e) => {
|
|
||||||
const hasGroupOrParent = e.group !== undefined || e.parent !== undefined
|
|
||||||
const groupMatched =
|
|
||||||
e.group !== undefined && e.parent !== undefined
|
|
||||||
? i.options
|
|
||||||
.find((o) => o.name === e.group)
|
|
||||||
?.options?.find((o) => o.name === e.name) !== undefined
|
|
||||||
: true
|
|
||||||
const subMatched =
|
|
||||||
e.group === undefined && e.parent !== undefined
|
|
||||||
? i.options.find((o) => o.name === e.name) !== undefined
|
|
||||||
: true
|
|
||||||
const nameMatched1 = e.name === i.name
|
|
||||||
const parentMatched = hasGroupOrParent ? e.parent === i.name : true
|
|
||||||
const nameMatched = hasGroupOrParent ? parentMatched : nameMatched1
|
|
||||||
|
|
||||||
const matched = groupMatched && subMatched && nameMatched
|
|
||||||
return matched
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Process an incoming Slash Command (interaction) */
|
|
||||||
private _process(interaction: Interaction): void {
|
|
||||||
if (!this.enabled) return
|
|
||||||
|
|
||||||
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return
|
|
||||||
|
|
||||||
const cmd = this._getCommand(interaction)
|
|
||||||
if (cmd?.group !== undefined)
|
|
||||||
interaction.data.options = interaction.data.options[0].options ?? []
|
|
||||||
if (cmd?.parent !== undefined)
|
|
||||||
interaction.data.options = interaction.data.options[0].options ?? []
|
|
||||||
|
|
||||||
if (cmd === undefined) return
|
|
||||||
|
|
||||||
cmd.handler(interaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyKey(
|
|
||||||
rawBody: string | Uint8Array | Buffer,
|
|
||||||
signature: string,
|
|
||||||
timestamp: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
if (this.publicKey === undefined)
|
|
||||||
throw new Error('Public Key is not present')
|
|
||||||
return edverify(
|
|
||||||
signature,
|
|
||||||
Buffer.concat([
|
|
||||||
Buffer.from(timestamp, 'utf-8'),
|
|
||||||
Buffer.from(
|
|
||||||
rawBody instanceof Uint8Array
|
|
||||||
? new TextDecoder().decode(rawBody)
|
|
||||||
: rawBody
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
this.publicKey
|
|
||||||
).catch(() => false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyOpineRequest(req: any): Promise<boolean> {
|
|
||||||
const signature = req.headers.get('x-signature-ed25519')
|
|
||||||
const timestamp = req.headers.get('x-signature-timestamp')
|
|
||||||
const contentLength = req.headers.get('content-length')
|
|
||||||
|
|
||||||
if (signature === null || timestamp === null || contentLength === null)
|
|
||||||
return false
|
|
||||||
|
|
||||||
const body = new Uint8Array(parseInt(contentLength))
|
|
||||||
await req.body.read(body)
|
|
||||||
|
|
||||||
const verified = await this.verifyKey(body, signature, timestamp)
|
|
||||||
if (!verified) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Middleware to verify request in Opine framework. */
|
|
||||||
async verifyOpineMiddleware(
|
|
||||||
req: any,
|
|
||||||
res: any,
|
|
||||||
next: CallableFunction
|
|
||||||
): Promise<any> {
|
|
||||||
const verified = await this.verifyOpineRequest(req)
|
|
||||||
if (!verified) return res.setStatus(401).end()
|
|
||||||
|
|
||||||
await next()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: create verifyOakMiddleware too
|
|
||||||
/** Method to verify Request from Oak server "Context". */
|
|
||||||
async verifyOakRequest(ctx: any): Promise<any> {
|
|
||||||
const signature = ctx.request.headers.get('x-signature-ed25519')
|
|
||||||
const timestamp = ctx.request.headers.get('x-signature-timestamp')
|
|
||||||
const contentLength = ctx.request.headers.get('content-length')
|
|
||||||
|
|
||||||
if (
|
|
||||||
signature === null ||
|
|
||||||
timestamp === null ||
|
|
||||||
contentLength === null ||
|
|
||||||
ctx.request.hasBody !== true
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await ctx.request.body().value
|
|
||||||
|
|
||||||
const verified = await this.verifyKey(body, signature, timestamp)
|
|
||||||
if (!verified) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SlashCommandHandler } from './slashClient.ts'
|
import type { SlashCommandHandler } from './slashClient.ts'
|
||||||
|
|
||||||
export class SlashModule {
|
export class SlashModule {
|
||||||
name: string = ''
|
name: string = ''
|
|
@ -1,4 +1,4 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,7 +63,9 @@ export class BaseManager<T, T2> {
|
||||||
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
|
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
|
||||||
const arr = (await this.array()) ?? []
|
const arr = (await this.array()) ?? []
|
||||||
const { readable, writable } = new TransformStream()
|
const { readable, writable } = new TransformStream()
|
||||||
arr.forEach((el) => writable.getWriter().write(el))
|
const writer = writable.getWriter()
|
||||||
|
arr.forEach((el: unknown) => writer.write(el))
|
||||||
|
writer.close()
|
||||||
yield* readable
|
yield* readable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
@ -43,7 +43,9 @@ export class BaseChildManager<T, T2> {
|
||||||
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
|
async *[Symbol.asyncIterator](): AsyncIterableIterator<T2> {
|
||||||
const arr = (await this.array()) ?? []
|
const arr = (await this.array()) ?? []
|
||||||
const { readable, writable } = new TransformStream()
|
const { readable, writable } = new TransformStream()
|
||||||
arr.forEach((el: unknown) => writable.getWriter().write(el))
|
const writer = writable.getWriter()
|
||||||
|
arr.forEach((el: unknown) => writer.write(el))
|
||||||
|
writer.close()
|
||||||
yield* readable
|
yield* readable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Client } from '../models/client.ts'
|
import { Client } from '../client/mod.ts'
|
||||||
import { Channel } from '../structures/channel.ts'
|
import { Channel } from '../structures/channel.ts'
|
||||||
import { Embed } from '../structures/embed.ts'
|
import { Embed } from '../structures/embed.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
import { Message } from '../structures/message.ts'
|
||||||
import { TextChannel } from '../structures/textChannel.ts'
|
import type { TextChannel } from '../structures/textChannel.ts'
|
||||||
import {
|
import type {
|
||||||
ChannelPayload,
|
ChannelPayload,
|
||||||
GuildChannelPayload,
|
GuildChannelPayload,
|
||||||
MessageOptions
|
MessageOptions
|
||||||
} from '../types/channel.ts'
|
} from '../types/channel.ts'
|
||||||
import { CHANNEL } from '../types/endpoint.ts'
|
import { CHANNEL } from '../types/endpoint.ts'
|
||||||
import getChannelByType from '../utils/getChannelByType.ts'
|
import getChannelByType from '../utils/channel.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export type AllMessageOptions = MessageOptions | Embed
|
export type AllMessageOptions = MessageOptions | Embed
|
||||||
|
@ -121,6 +121,10 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload.content === undefined && payload.embed === undefined) {
|
||||||
|
payload.content = ''
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await this.client.rest.api.channels[channelID].messages.post(
|
const resp = await this.client.rest.api.channels[channelID].messages.post(
|
||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
import { EmojiPayload } from '../types/emoji.ts'
|
import type { EmojiPayload } from '../types/emoji.ts'
|
||||||
import { GUILD_EMOJI } from '../types/endpoint.ts'
|
import { GUILD_EMOJI } from '../types/endpoint.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache Manager used for Caching values related to Gateway connection
|
* Cache Manager used for Caching values related to Gateway connection
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { VoiceStatePayload } from '../types/voice.ts'
|
import type { VoiceStatePayload } from '../types/voice.ts'
|
||||||
import { VoiceState } from '../structures/voiceState.ts'
|
import { VoiceState } from '../structures/voiceState.ts'
|
||||||
import { GuildVoiceStatesManager } from './guildVoiceStates.ts'
|
import { GuildVoiceStatesManager } from './guildVoiceStates.ts'
|
||||||
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
|
|
||||||
export class GuildChannelVoiceStatesManager extends BaseChildManager<
|
export class GuildChannelVoiceStatesManager extends BaseChildManager<
|
||||||
VoiceStatePayload,
|
VoiceStatePayload,
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Channel } from '../structures/channel.ts'
|
import { Channel } from '../structures/channel.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { Guild } from '../structures/guild.ts'
|
||||||
import { CategoryChannel } from '../structures/guildCategoryChannel.ts'
|
import type { CategoryChannel } from '../structures/guildCategoryChannel.ts'
|
||||||
import {
|
import {
|
||||||
ChannelTypes,
|
ChannelTypes,
|
||||||
GuildChannelPayload,
|
GuildChannelPayload,
|
||||||
OverwritePayload
|
OverwritePayload
|
||||||
} from '../types/channel.ts'
|
} from '../types/channel.ts'
|
||||||
import { GuildChannels, GuildChannelPayloads } from '../types/guild.ts'
|
import type { GuildChannels, GuildChannelPayloads } from '../types/guild.ts'
|
||||||
import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts'
|
import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { ChannelsManager } from './channels.ts'
|
import type { ChannelsManager } from './channels.ts'
|
||||||
|
|
||||||
export interface CreateChannelOptions {
|
export interface CreateChannelOptions {
|
||||||
name: string
|
name: string
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { EmojiPayload } from '../types/emoji.ts'
|
import type { EmojiPayload } from '../types/emoji.ts'
|
||||||
import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts'
|
import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { EmojisManager } from './emojis.ts'
|
import type { EmojisManager } from './emojis.ts'
|
||||||
import { fetchAuto } from '../../deps.ts'
|
import { fetchAuto } from '../../deps.ts'
|
||||||
|
|
||||||
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
|
export class GuildEmojisManager extends BaseChildManager<EmojiPayload, Emoji> {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { VoiceState } from '../structures/voiceState.ts'
|
import { VoiceState } from '../structures/voiceState.ts'
|
||||||
import { VoiceStatePayload } from '../types/voice.ts'
|
import type { VoiceStatePayload } from '../types/voice.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class GuildVoiceStatesManager extends BaseManager<
|
export class GuildVoiceStatesManager extends BaseManager<
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { fetchAuto } from '../../deps.ts'
|
import { fetchAuto } from '../../deps.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { Guild } from '../structures/guild.ts'
|
||||||
import { Template } from '../structures/template.ts'
|
import type { Template } from '../structures/template.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
|
import { GUILD, GUILDS, GUILD_PREVIEW } from '../types/endpoint.ts'
|
||||||
import {
|
import type {
|
||||||
GuildPayload,
|
GuildPayload,
|
||||||
MemberPayload,
|
MemberPayload,
|
||||||
GuildCreateRolePayload,
|
GuildCreateRolePayload,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { GuildTextChannel, User } from '../../mod.ts'
|
import type { GuildTextChannel, User } from '../../mod.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Invite } from '../structures/invite.ts'
|
import { Invite } from '../structures/invite.ts'
|
||||||
import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts'
|
import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts'
|
||||||
import { InvitePayload } from '../types/invite.ts'
|
import type { InvitePayload } from '../types/invite.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export enum InviteTargetUserType {
|
export enum InviteTargetUserType {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { BaseChildManager } from './baseChild.ts'
|
import { BaseChildManager } from './baseChild.ts'
|
||||||
import { RolePayload } from '../types/role.ts'
|
import type { RolePayload } from '../types/role.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { Member } from '../structures/member.ts'
|
import type { Member } from '../structures/member.ts'
|
||||||
import { RolesManager } from './roles.ts'
|
import type { RolesManager } from './roles.ts'
|
||||||
import { MemberPayload } from '../types/guild.ts'
|
import type { MemberPayload } from '../types/guild.ts'
|
||||||
import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts'
|
import { GUILD_MEMBER_ROLE } from '../types/endpoint.ts'
|
||||||
|
|
||||||
export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
|
export class MemberRolesManager extends BaseChildManager<RolePayload, Role> {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Member } from '../structures/member.ts'
|
import { Member } from '../structures/member.ts'
|
||||||
import { GUILD_MEMBER } from '../types/endpoint.ts'
|
import { GUILD_MEMBER } from '../types/endpoint.ts'
|
||||||
import { MemberPayload } from '../types/guild.ts'
|
import type { MemberPayload } from '../types/guild.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
import { Permissions } from '../utils/permissions.ts'
|
import { Permissions } from '../utils/permissions.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Emoji } from '../structures/emoji.ts'
|
import { Emoji } from '../structures/emoji.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Message } from '../structures/message.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
|
||||||
import { MessageReaction } from '../structures/messageReaction.ts'
|
import { MessageReaction } from '../structures/messageReaction.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { Reaction } from '../types/channel.ts'
|
import type { Reaction } from '../types/channel.ts'
|
||||||
import {
|
import {
|
||||||
MESSAGE_REACTION,
|
MESSAGE_REACTION,
|
||||||
MESSAGE_REACTIONS,
|
MESSAGE_REACTIONS,
|
||||||
|
@ -19,7 +18,7 @@ export class MessageReactionsManager extends BaseManager<
|
||||||
message: Message
|
message: Message
|
||||||
|
|
||||||
constructor(client: Client, message: Message) {
|
constructor(client: Client, message: Message) {
|
||||||
super(client, `reactions:${message.id}`, Guild)
|
super(client, `reactions:${message.id}`, MessageReaction)
|
||||||
this.message = message
|
this.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Message } from '../structures/message.ts'
|
import { Message } from '../structures/message.ts'
|
||||||
import { TextChannel } from '../structures/textChannel.ts'
|
import type { TextChannel } from '../structures/textChannel.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { MessagePayload } from '../types/channel.ts'
|
import type { MessagePayload } from '../types/channel.ts'
|
||||||
import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
|
import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Presence } from '../structures/presence.ts'
|
import { Presence } from '../structures/presence.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { PresenceUpdatePayload } from '../types/gateway.ts'
|
import type { PresenceUpdatePayload } from '../types/gateway.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class GuildPresencesManager extends BaseManager<
|
export class GuildPresencesManager extends BaseManager<
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { MessageReaction } from '../structures/messageReaction.ts'
|
import type { MessageReaction } from '../structures/messageReaction.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import type { User } from '../structures/user.ts'
|
||||||
import { UsersManager } from './users.ts'
|
import { UsersManager } from './users.ts'
|
||||||
|
|
||||||
export class ReactionUsersManager extends UsersManager {
|
export class ReactionUsersManager extends UsersManager {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Permissions } from '../../mod.ts'
|
import { Permissions } from '../../mod.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import type { Guild } from '../structures/guild.ts'
|
||||||
import { Role } from '../structures/role.ts'
|
import { Role } from '../structures/role.ts'
|
||||||
import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts'
|
import { GUILD_ROLE, GUILD_ROLES } from '../types/endpoint.ts'
|
||||||
import { RoleModifyPayload, RolePayload } from '../types/role.ts'
|
import type { RoleModifyPayload, RolePayload } from '../types/role.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export interface CreateGuildRoleOptions {
|
export interface CreateGuildRoleOptions {
|
||||||
|
@ -22,14 +22,17 @@ export class RolesManager extends BaseManager<RolePayload, Role> {
|
||||||
this.guild = guild
|
this.guild = guild
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch a Guild Role (from API) */
|
/** Fetch All Guild Roles */
|
||||||
async fetch(id: string): Promise<Role> {
|
async fetchAll(): Promise<Role[]> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
this.client.rest
|
this.client.rest.api.guilds[this.guild.id].roles.get
|
||||||
.get(GUILD_ROLE(this.guild.id, id))
|
.then(async (data: RolePayload[]) => {
|
||||||
.then(async (data) => {
|
const roles: Role[] = []
|
||||||
await this.set(id, data as RolePayload)
|
for (const raw of data) {
|
||||||
resolve(((await this.get(id)) as unknown) as Role)
|
await this.set(raw.id, raw)
|
||||||
|
roles.push(new Role(this.client, raw, this.guild))
|
||||||
|
}
|
||||||
|
resolve(roles)
|
||||||
})
|
})
|
||||||
.catch((e) => reject(e))
|
.catch((e) => reject(e))
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { USER } from '../types/endpoint.ts'
|
import { USER } from '../types/endpoint.ts'
|
||||||
import { UserPayload } from '../types/user.ts'
|
import type { UserPayload } from '../types/user.ts'
|
||||||
import { BaseManager } from './base.ts'
|
import { BaseManager } from './base.ts'
|
||||||
|
|
||||||
export class UsersManager extends BaseManager<UserPayload, User> {
|
export class UsersManager extends BaseManager<UserPayload, User> {
|
||||||
|
|
|
@ -1,676 +0,0 @@
|
||||||
import * as baseEndpoints from '../consts/urlsAndVersions.ts'
|
|
||||||
import { Embed } from '../structures/embed.ts'
|
|
||||||
import { MessageAttachment } from '../structures/message.ts'
|
|
||||||
import { Collection } from '../utils/collection.ts'
|
|
||||||
import { Client } from './client.ts'
|
|
||||||
import { simplifyAPIError } from '../utils/err_fmt.ts'
|
|
||||||
|
|
||||||
export type RequestMethods =
|
|
||||||
| 'get'
|
|
||||||
| 'post'
|
|
||||||
| 'put'
|
|
||||||
| 'patch'
|
|
||||||
| 'head'
|
|
||||||
| 'delete'
|
|
||||||
|
|
||||||
export enum HttpResponseCode {
|
|
||||||
Ok = 200,
|
|
||||||
Created = 201,
|
|
||||||
NoContent = 204,
|
|
||||||
NotModified = 304,
|
|
||||||
BadRequest = 400,
|
|
||||||
Unauthorized = 401,
|
|
||||||
Forbidden = 403,
|
|
||||||
NotFound = 404,
|
|
||||||
MethodNotAllowed = 405,
|
|
||||||
TooManyRequests = 429,
|
|
||||||
GatewayUnavailable = 502
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestHeaders {
|
|
||||||
[name: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiscordAPIErrorPayload {
|
|
||||||
url: string
|
|
||||||
status: number
|
|
||||||
method: string
|
|
||||||
code?: number
|
|
||||||
message?: string
|
|
||||||
errors: object
|
|
||||||
requestData: { [key: string]: any }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DiscordAPIError extends Error {
|
|
||||||
name = 'DiscordAPIError'
|
|
||||||
error?: DiscordAPIErrorPayload
|
|
||||||
|
|
||||||
constructor(error: string | DiscordAPIErrorPayload) {
|
|
||||||
super()
|
|
||||||
const fmt = Object.entries(
|
|
||||||
typeof error === 'object' ? simplifyAPIError(error.errors) : {}
|
|
||||||
)
|
|
||||||
this.message =
|
|
||||||
typeof error === 'string'
|
|
||||||
? `${error} `
|
|
||||||
: `\n${error.method} ${error.url.slice(7)} returned ${error.status}\n(${
|
|
||||||
error.code ?? 'unknown'
|
|
||||||
}) ${error.message}${
|
|
||||||
fmt.length === 0
|
|
||||||
? ''
|
|
||||||
: `\n${fmt
|
|
||||||
.map(
|
|
||||||
(e) =>
|
|
||||||
` at ${e[0]}:\n${e[1]
|
|
||||||
.map((e) => ` - ${e}`)
|
|
||||||
.join('\n')}`
|
|
||||||
)
|
|
||||||
.join('\n')}\n`
|
|
||||||
}`
|
|
||||||
if (typeof error === 'object') this.error = error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueuedItem {
|
|
||||||
bucket?: string | null
|
|
||||||
url: string
|
|
||||||
onComplete: () => Promise<
|
|
||||||
| {
|
|
||||||
rateLimited: any
|
|
||||||
bucket?: string | null
|
|
||||||
before: boolean
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RateLimit {
|
|
||||||
url: string
|
|
||||||
resetAt: number
|
|
||||||
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 {
|
|
||||||
/** Make a GET request to current route */
|
|
||||||
get: APIMap
|
|
||||||
/** Make a POST request to current route */
|
|
||||||
post: APIMap
|
|
||||||
/** Make a PATCH request to current route */
|
|
||||||
patch: APIMap
|
|
||||||
/** Make a PUT request to current route */
|
|
||||||
put: APIMap
|
|
||||||
/** Make a DELETE request to current route */
|
|
||||||
delete: APIMap
|
|
||||||
/** Make a HEAD request to current route */
|
|
||||||
head: APIMap
|
|
||||||
/** Continue building API Route */
|
|
||||||
[name: string]: APIMap
|
|
||||||
}
|
|
||||||
|
|
||||||
/** API Route builder function */
|
|
||||||
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 to use for authorization */
|
|
||||||
token?: string | (() => string | undefined)
|
|
||||||
/** Headers to patch with if any */
|
|
||||||
headers?: { [name: string]: string | undefined }
|
|
||||||
/** Whether to use Canary instance of Discord API or not */
|
|
||||||
canary?: boolean
|
|
||||||
/** Discord REST API version to use */
|
|
||||||
version?: 6 | 7 | 8
|
|
||||||
/** Token Type to use for Authorization */
|
|
||||||
tokenType?: TokenType
|
|
||||||
/** User Agent to use (Header) */
|
|
||||||
userAgent?: string
|
|
||||||
/** Optional Harmony client */
|
|
||||||
client?: Client
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Token Type for REST API. */
|
|
||||||
export enum TokenType {
|
|
||||||
/** Token type for Bot User */
|
|
||||||
Bot = 'Bot',
|
|
||||||
/** Token Type for OAuth2 */
|
|
||||||
Bearer = 'Bearer',
|
|
||||||
/** No Token Type. Can be used for User accounts. */
|
|
||||||
None = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An easier to use interface for interacting with Discord REST API. */
|
|
||||||
export class RESTManager {
|
|
||||||
queues: { [key: string]: QueuedItem[] } = {}
|
|
||||||
rateLimits = new Collection<string, RateLimit>()
|
|
||||||
/** Whether we are globally ratelimited or not */
|
|
||||||
globalRateLimit: boolean = false
|
|
||||||
/** Whether requests are being processed or not */
|
|
||||||
processing: boolean = false
|
|
||||||
/** API Version being used by REST Manager */
|
|
||||||
version: number = 8
|
|
||||||
/**
|
|
||||||
* API Map - easy to use way for interacting with Discord API.
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* * ```ts
|
|
||||||
* rest.api.users['123'].get().then(userPayload => doSomething)
|
|
||||||
* ```
|
|
||||||
* * ```ts
|
|
||||||
* rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {})
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
api: APIMap
|
|
||||||
/** Token being used for Authorization */
|
|
||||||
token?: string | (() => string | undefined)
|
|
||||||
/** Token Type of the Token if any */
|
|
||||||
tokenType: TokenType = TokenType.Bot
|
|
||||||
/** Headers object which patch the current ones */
|
|
||||||
headers: any = {}
|
|
||||||
/** Optional custom User Agent (header) */
|
|
||||||
userAgent?: string
|
|
||||||
/** Whether REST Manager is using Canary API */
|
|
||||||
canary?: boolean
|
|
||||||
/** Optional Harmony Client object */
|
|
||||||
client?: Client
|
|
||||||
|
|
||||||
constructor(options?: RESTOptions) {
|
|
||||||
this.api = builder(this)
|
|
||||||
if (options?.token !== undefined) this.token = options.token
|
|
||||||
if (options?.version !== undefined) this.version = options.version
|
|
||||||
if (options?.headers !== undefined) this.headers = options.headers
|
|
||||||
if (options?.tokenType !== undefined) this.tokenType = options.tokenType
|
|
||||||
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
|
|
||||||
if (options?.canary !== undefined) this.canary = options.canary
|
|
||||||
if (options?.client !== undefined) this.client = options.client
|
|
||||||
this.handleRateLimits()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Checks the queues of buckets, if empty, delete entry */
|
|
||||||
private checkQueues(): void {
|
|
||||||
Object.entries(this.queues).forEach(([key, value]) => {
|
|
||||||
if (value.length === 0) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete this.queues[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Adds a Request to Queue */
|
|
||||||
private queue(request: QueuedItem): void {
|
|
||||||
const route = request.url.substring(
|
|
||||||
Number(baseEndpoints.DISCORD_API_URL.length) + 1
|
|
||||||
)
|
|
||||||
const parts = route.split('/')
|
|
||||||
parts.shift()
|
|
||||||
const [id] = parts
|
|
||||||
|
|
||||||
if (this.queues[id] !== undefined) {
|
|
||||||
this.queues[id].push(request)
|
|
||||||
} else {
|
|
||||||
this.queues[id] = [request]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processQueue(): Promise<void> {
|
|
||||||
if (Object.keys(this.queues).length !== 0 && !this.globalRateLimit) {
|
|
||||||
await Promise.allSettled(
|
|
||||||
Object.values(this.queues).map(async (pathQueue) => {
|
|
||||||
const request = pathQueue.shift()
|
|
||||||
if (request === undefined) return
|
|
||||||
|
|
||||||
const rateLimitedURLResetIn = await this.isRateLimited(request.url)
|
|
||||||
|
|
||||||
if (typeof request.bucket === 'string') {
|
|
||||||
const rateLimitResetIn = await this.isRateLimited(request.bucket)
|
|
||||||
if (rateLimitResetIn !== false) {
|
|
||||||
this.queue(request)
|
|
||||||
} else {
|
|
||||||
const result = await request.onComplete()
|
|
||||||
if (result?.rateLimited !== undefined) {
|
|
||||||
this.queue({
|
|
||||||
...request,
|
|
||||||
bucket: result.bucket ?? request.bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rateLimitedURLResetIn !== false) {
|
|
||||||
this.queue(request)
|
|
||||||
} else {
|
|
||||||
const result = await request.onComplete()
|
|
||||||
if (result?.rateLimited !== undefined) {
|
|
||||||
this.queue({
|
|
||||||
...request,
|
|
||||||
bucket: result.bucket ?? request.bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(this.queues).length !== 0) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.processQueue()
|
|
||||||
this.checkQueues()
|
|
||||||
} else this.processing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private prepare(body: any, method: RequestMethods): { [key: string]: any } {
|
|
||||||
const headers: RequestHeaders = {
|
|
||||||
'User-Agent':
|
|
||||||
this.userAgent ??
|
|
||||||
`DiscordBot (harmony, https://github.com/harmonyland/harmony)`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.token !== undefined) {
|
|
||||||
const token = typeof this.token === 'string' ? this.token : this.token()
|
|
||||||
if (token !== undefined)
|
|
||||||
headers.Authorization = `${this.tokenType} ${token}`.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method === 'get' || method === 'head' || method === 'delete')
|
|
||||||
body = undefined
|
|
||||||
|
|
||||||
if (body?.reason !== undefined) {
|
|
||||||
headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
let _files: undefined | MessageAttachment[]
|
|
||||||
if (body?.embed?.files !== undefined && Array.isArray(body?.embed?.files)) {
|
|
||||||
_files = body?.embed?.files
|
|
||||||
}
|
|
||||||
if (body?.embeds !== undefined && Array.isArray(body?.embeds)) {
|
|
||||||
const files1 = body?.embeds
|
|
||||||
.map((e: Embed) => e.files)
|
|
||||||
.filter((e: MessageAttachment[]) => e !== undefined)
|
|
||||||
for (const files of files1) {
|
|
||||||
for (const file of files) {
|
|
||||||
if (_files === undefined) _files = []
|
|
||||||
_files?.push(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
body?.file !== undefined ||
|
|
||||||
body?.files !== undefined ||
|
|
||||||
_files !== undefined
|
|
||||||
) {
|
|
||||||
const files: Array<{ blob: Blob; name: string }> = []
|
|
||||||
if (body?.file !== undefined) files.push(body.file)
|
|
||||||
if (body?.files !== undefined && Array.isArray(body.files)) {
|
|
||||||
for (const file of body.files) {
|
|
||||||
files.push(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_files !== undefined) {
|
|
||||||
for (const file of _files) {
|
|
||||||
files.push(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const form = new FormData()
|
|
||||||
files.forEach((file, index) =>
|
|
||||||
form.append(`file${index + 1}`, file.blob, file.name)
|
|
||||||
)
|
|
||||||
const json = JSON.stringify(body)
|
|
||||||
form.append('payload_json', json)
|
|
||||||
if (body === undefined) body = {}
|
|
||||||
body.file = form
|
|
||||||
} else if (
|
|
||||||
body !== undefined &&
|
|
||||||
!['get', 'delete'].includes(method.toLowerCase())
|
|
||||||
) {
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.headers !== undefined) Object.assign(headers, this.headers)
|
|
||||||
const data: { [name: string]: any } = {
|
|
||||||
headers,
|
|
||||||
body: body?.file ?? JSON.stringify(body),
|
|
||||||
method: method.toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private isRateLimited(url: string): number | false {
|
|
||||||
const global = this.rateLimits.get('global')
|
|
||||||
const rateLimited = this.rateLimits.get(url)
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
if (rateLimited !== undefined && now < rateLimited.resetAt) {
|
|
||||||
return rateLimited.resetAt - now
|
|
||||||
}
|
|
||||||
if (global !== undefined && now < global.resetAt) {
|
|
||||||
return global.resetAt - now
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Processes headers of the Response */
|
|
||||||
private processHeaders(
|
|
||||||
url: string,
|
|
||||||
headers: Headers
|
|
||||||
): string | null | undefined {
|
|
||||||
let rateLimited = false
|
|
||||||
|
|
||||||
const global = headers.get('x-ratelimit-global')
|
|
||||||
const bucket = headers.get('x-ratelimit-bucket')
|
|
||||||
const remaining = headers.get('x-ratelimit-remaining')
|
|
||||||
const resetAt = headers.get('x-ratelimit-reset')
|
|
||||||
const retryAfter = headers.get('retry-after')
|
|
||||||
|
|
||||||
if (remaining !== null && remaining === '0') {
|
|
||||||
rateLimited = true
|
|
||||||
|
|
||||||
this.rateLimits.set(url, {
|
|
||||||
url,
|
|
||||||
resetAt: Number(resetAt) * 1000,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
|
|
||||||
if (bucket !== null) {
|
|
||||||
this.rateLimits.set(bucket, {
|
|
||||||
url,
|
|
||||||
resetAt: Number(resetAt) * 1000,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global !== null) {
|
|
||||||
const reset = Date.now() + Number(retryAfter)
|
|
||||||
this.globalRateLimit = true
|
|
||||||
rateLimited = true
|
|
||||||
|
|
||||||
this.rateLimits.set('global', {
|
|
||||||
url: 'global',
|
|
||||||
resetAt: reset,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
|
|
||||||
if (bucket !== null) {
|
|
||||||
this.rateLimits.set(bucket, {
|
|
||||||
url: 'global',
|
|
||||||
resetAt: reset,
|
|
||||||
bucket
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rateLimited ? bucket : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Handles status code of response and acts as required */
|
|
||||||
private handleStatusCode(
|
|
||||||
response: Response,
|
|
||||||
body: any,
|
|
||||||
data: { [key: string]: any },
|
|
||||||
reject: CallableFunction
|
|
||||||
): void {
|
|
||||||
const status = response.status
|
|
||||||
|
|
||||||
// We have hit ratelimit - this should not happen
|
|
||||||
if (status === HttpResponseCode.TooManyRequests) {
|
|
||||||
if (this.client !== undefined)
|
|
||||||
this.client.emit('rateLimit', {
|
|
||||||
method: data.method,
|
|
||||||
url: response.url,
|
|
||||||
body
|
|
||||||
})
|
|
||||||
reject(new Error('RateLimited'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's a normal status code... just continue
|
|
||||||
if (
|
|
||||||
(status >= 200 && status < 400) ||
|
|
||||||
status === HttpResponseCode.NoContent
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
let text: undefined | string = Deno.inspect(
|
|
||||||
body.errors === undefined ? body : body.errors
|
|
||||||
)
|
|
||||||
if (text === 'undefined') text = undefined
|
|
||||||
|
|
||||||
if (status === HttpResponseCode.Unauthorized)
|
|
||||||
reject(
|
|
||||||
new DiscordAPIError(`Request was Unauthorized. Invalid Token.\n${text}`)
|
|
||||||
)
|
|
||||||
|
|
||||||
const _data = { ...data }
|
|
||||||
if (_data?.headers !== undefined) delete _data.headers
|
|
||||||
if (_data?.method !== undefined) delete _data.method
|
|
||||||
|
|
||||||
// At this point we know it is error
|
|
||||||
const error: DiscordAPIErrorPayload = {
|
|
||||||
url: new URL(response.url).pathname,
|
|
||||||
status,
|
|
||||||
method: data.method,
|
|
||||||
code: body?.code,
|
|
||||||
message: body?.message,
|
|
||||||
errors: body?.errors ?? {},
|
|
||||||
requestData: _data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
HttpResponseCode.BadRequest,
|
|
||||||
HttpResponseCode.NotFound,
|
|
||||||
HttpResponseCode.Forbidden,
|
|
||||||
HttpResponseCode.MethodNotAllowed
|
|
||||||
].includes(status)
|
|
||||||
) {
|
|
||||||
reject(new DiscordAPIError(error))
|
|
||||||
} else if (status === HttpResponseCode.GatewayUnavailable) {
|
|
||||||
reject(new DiscordAPIError(error))
|
|
||||||
} else reject(new DiscordAPIError('Request - Unknown Error'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a Request to Discord API.
|
|
||||||
* @param method HTTP Method to use
|
|
||||||
* @param url URL of the Request
|
|
||||||
* @param body Body to send with Request
|
|
||||||
* @param maxRetries Number of Max Retries to perform
|
|
||||||
* @param bucket BucketID of the Request
|
|
||||||
* @param rawResponse Whether to get Raw Response or body itself
|
|
||||||
*/
|
|
||||||
async make(
|
|
||||||
method: RequestMethods,
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const onComplete = async (): Promise<undefined | any> => {
|
|
||||||
try {
|
|
||||||
const rateLimitResetIn = await this.isRateLimited(url)
|
|
||||||
if (rateLimitResetIn !== false) {
|
|
||||||
return {
|
|
||||||
rateLimited: rateLimitResetIn,
|
|
||||||
before: true,
|
|
||||||
bucket
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query =
|
|
||||||
method === 'get' && body !== undefined
|
|
||||||
? Object.entries(body as any)
|
|
||||||
.filter(([k, v]) => v !== undefined)
|
|
||||||
.map(
|
|
||||||
([key, value]) =>
|
|
||||||
`${encodeURIComponent(key)}=${encodeURIComponent(
|
|
||||||
value as any
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
.join('&')
|
|
||||||
: ''
|
|
||||||
let urlToUse =
|
|
||||||
method === 'get' && query !== '' ? `${url}?${query}` : url
|
|
||||||
|
|
||||||
// It doesn't start with HTTP, that means it's an incomplete URL
|
|
||||||
if (!urlToUse.startsWith('http')) {
|
|
||||||
if (!urlToUse.startsWith('/')) urlToUse = `/${urlToUse}`
|
|
||||||
urlToUse =
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
||||||
baseEndpoints.DISCORD_API_URL +
|
|
||||||
'/v' +
|
|
||||||
baseEndpoints.DISCORD_API_VERSION +
|
|
||||||
urlToUse
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canary === true && urlToUse.startsWith('http')) {
|
|
||||||
const split = urlToUse.split('//')
|
|
||||||
urlToUse = split[0] + '//canary.' + split[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestData = this.prepare(body, method)
|
|
||||||
|
|
||||||
const response = await fetch(urlToUse, requestData)
|
|
||||||
const bucketFromHeaders = this.processHeaders(url, response.headers)
|
|
||||||
|
|
||||||
if (response.status === 204)
|
|
||||||
return resolve(
|
|
||||||
rawResponse === true ? { response, body: null } : undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
const json: any = await response.json()
|
|
||||||
await this.handleStatusCode(response, json, requestData, reject)
|
|
||||||
|
|
||||||
if (
|
|
||||||
json.retry_after !== undefined ||
|
|
||||||
json.message === 'You are being rate limited.'
|
|
||||||
) {
|
|
||||||
if (maxRetries > 10) {
|
|
||||||
throw new Error('Max RateLimit Retries hit')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rateLimited: json.retry_after,
|
|
||||||
before: false,
|
|
||||||
bucket: bucketFromHeaders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolve(rawResponse === true ? { response, body: json } : json)
|
|
||||||
} catch (error) {
|
|
||||||
return reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queue({
|
|
||||||
onComplete,
|
|
||||||
bucket,
|
|
||||||
url
|
|
||||||
})
|
|
||||||
if (!this.processing) {
|
|
||||||
this.processing = true
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.processQueue()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Checks for RateLimits times and deletes if already over */
|
|
||||||
private handleRateLimits(): void {
|
|
||||||
const now = Date.now()
|
|
||||||
this.rateLimits.forEach((value, key) => {
|
|
||||||
// Ratelimit has not ended
|
|
||||||
if (value.resetAt > now) return
|
|
||||||
// It ended, so delete
|
|
||||||
this.rateLimits.delete(key)
|
|
||||||
if (key === 'global') this.globalRateLimit = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a GET Request to API */
|
|
||||||
async get(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('get', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a POST Request to API */
|
|
||||||
async post(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('post', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a DELETE Request to API */
|
|
||||||
async delete(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('delete', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a PATCH Request to API */
|
|
||||||
async patch(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('patch', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Makes a PUT Request to API */
|
|
||||||
async put(
|
|
||||||
url: string,
|
|
||||||
body?: unknown,
|
|
||||||
maxRetries = 0,
|
|
||||||
bucket?: string | null,
|
|
||||||
rawResponse?: boolean
|
|
||||||
): Promise<any> {
|
|
||||||
return await this.make('put', url, body, maxRetries, bucket, rawResponse)
|
|
||||||
}
|
|
||||||
}
|
|
239
src/rest/bucket.ts
Normal file
239
src/rest/bucket.ts
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
// based on https://github.com/discordjs/discord.js/blob/master/src/rest/RequestHandler.js
|
||||||
|
// adapted to work with harmony rest manager
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
|
|
||||||
|
import { delay } from '../utils/delay.ts'
|
||||||
|
import { DiscordAPIError, HTTPError } from './error.ts'
|
||||||
|
import type { RESTManager } from './manager.ts'
|
||||||
|
import { RequestQueue } from './queue.ts'
|
||||||
|
import { APIRequest } from './request.ts'
|
||||||
|
|
||||||
|
function parseResponse(res: Response, raw: boolean): any {
|
||||||
|
if (raw) return res
|
||||||
|
if (res.status === 204) return undefined
|
||||||
|
if (res.headers.get('content-type')?.startsWith('application/json') === true)
|
||||||
|
return res.json()
|
||||||
|
return res.arrayBuffer().then((e) => new Uint8Array(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAPIOffset(serverDate: number | string): number {
|
||||||
|
return new Date(serverDate).getTime() - Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateReset(
|
||||||
|
reset: number | string,
|
||||||
|
serverDate: number | string
|
||||||
|
): number {
|
||||||
|
return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
let invalidCount = 0
|
||||||
|
let invalidCountResetTime: number | null = null
|
||||||
|
|
||||||
|
export class BucketHandler {
|
||||||
|
queue = new RequestQueue()
|
||||||
|
reset = -1
|
||||||
|
remaining = -1
|
||||||
|
limit = -1
|
||||||
|
|
||||||
|
constructor(public manager: RESTManager) {}
|
||||||
|
|
||||||
|
async push(request: APIRequest): Promise<any> {
|
||||||
|
await this.queue.wait()
|
||||||
|
try {
|
||||||
|
return await this.execute(request)
|
||||||
|
} finally {
|
||||||
|
this.queue.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get globalLimited(): boolean {
|
||||||
|
return (
|
||||||
|
this.manager.globalRemaining <= 0 &&
|
||||||
|
Date.now() < Number(this.manager.globalReset)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get localLimited(): boolean {
|
||||||
|
return this.remaining <= 0 && Date.now() < this.reset
|
||||||
|
}
|
||||||
|
|
||||||
|
get limited(): boolean {
|
||||||
|
return this.globalLimited || this.localLimited
|
||||||
|
}
|
||||||
|
|
||||||
|
get inactive(): boolean {
|
||||||
|
return this.queue.remaining === 0 && !this.limited
|
||||||
|
}
|
||||||
|
|
||||||
|
async globalDelayFor(ms: number): Promise<void> {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
this.manager.setTimeout(() => {
|
||||||
|
this.manager.globalDelay = null
|
||||||
|
resolve()
|
||||||
|
}, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(request: APIRequest): Promise<any> {
|
||||||
|
while (this.limited) {
|
||||||
|
const isGlobal = this.globalLimited
|
||||||
|
let limit, timeout, delayPromise
|
||||||
|
|
||||||
|
if (isGlobal) {
|
||||||
|
limit = this.manager.globalLimit
|
||||||
|
timeout =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||||
|
Number(this.manager.globalReset) +
|
||||||
|
this.manager.restTimeOffset -
|
||||||
|
Date.now()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!this.manager.globalDelay) {
|
||||||
|
this.manager.globalDelay = this.globalDelayFor(timeout) as any
|
||||||
|
}
|
||||||
|
delayPromise = this.manager.globalDelay
|
||||||
|
} else {
|
||||||
|
limit = this.limit
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||||
|
timeout = this.reset + this.manager.restTimeOffset - Date.now()
|
||||||
|
delayPromise = delay(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.client?.emit('rateLimit', {
|
||||||
|
timeout,
|
||||||
|
limit,
|
||||||
|
method: request.method,
|
||||||
|
path: request.path,
|
||||||
|
global: isGlobal
|
||||||
|
})
|
||||||
|
|
||||||
|
await delayPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
|
||||||
|
this.manager.globalReset = Date.now() + 1000
|
||||||
|
this.manager.globalRemaining = this.manager.globalLimit
|
||||||
|
}
|
||||||
|
this.manager.globalRemaining--
|
||||||
|
|
||||||
|
// Perform the request
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = await request.execute()
|
||||||
|
} catch (error) {
|
||||||
|
if (request.retries === this.manager.retryLimit) {
|
||||||
|
throw new HTTPError(
|
||||||
|
error.message,
|
||||||
|
error.constructor.name,
|
||||||
|
error.status,
|
||||||
|
request.method,
|
||||||
|
request.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.retries++
|
||||||
|
return await this.execute(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sublimitTimeout
|
||||||
|
if (res?.headers !== undefined) {
|
||||||
|
const serverDate = res.headers.get('date')
|
||||||
|
const limit = res.headers.get('x-ratelimit-limit')
|
||||||
|
const remaining = res.headers.get('x-ratelimit-remaining')
|
||||||
|
const reset = res.headers.get('x-ratelimit-reset')
|
||||||
|
this.limit = limit !== null ? Number(limit) : Infinity
|
||||||
|
this.remaining = remaining !== null ? Number(remaining) : 1
|
||||||
|
this.reset =
|
||||||
|
reset !== null ? calculateReset(reset, serverDate!) : Date.now()
|
||||||
|
|
||||||
|
if (request.path.includes('reactions') === true) {
|
||||||
|
this.reset =
|
||||||
|
new Date(serverDate!).getTime() - getAPIOffset(serverDate!) + 250
|
||||||
|
}
|
||||||
|
|
||||||
|
let retryAfter: number | null | string = res.headers.get('retry-after')
|
||||||
|
retryAfter = retryAfter !== null ? Number(retryAfter) * 1000 : -1
|
||||||
|
if (retryAfter > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (res.headers.get('x-ratelimit-global')) {
|
||||||
|
this.manager.globalRemaining = 0
|
||||||
|
this.manager.globalReset = Date.now() + retryAfter
|
||||||
|
} else if (!this.localLimited) {
|
||||||
|
sublimitTimeout = retryAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 401 || res.status === 403 || res.status === 429) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
|
||||||
|
invalidCountResetTime = Date.now() + 1000 * 60 * 10
|
||||||
|
invalidCount = 0
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
invalidCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.ok === true) {
|
||||||
|
return parseResponse(res, request.options.rawResponse ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status >= 400 && res.status < 500) {
|
||||||
|
if (res.status === 429) {
|
||||||
|
this.manager.client?.emit(
|
||||||
|
'debug',
|
||||||
|
`Rate-Limited on route ${request.path}${
|
||||||
|
sublimitTimeout !== undefined ? ' for sublimit' : ''
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sublimitTimeout !== undefined) {
|
||||||
|
await delay(sublimitTimeout)
|
||||||
|
}
|
||||||
|
return await this.execute(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = await parseResponse(res, request.options.rawResponse ?? false)
|
||||||
|
} catch (err) {
|
||||||
|
throw new HTTPError(
|
||||||
|
err.message,
|
||||||
|
err.constructor.name,
|
||||||
|
err.status,
|
||||||
|
request.method,
|
||||||
|
request.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DiscordAPIError({
|
||||||
|
url: request.path,
|
||||||
|
errors: data?.errors,
|
||||||
|
status: res.status,
|
||||||
|
method: request.method,
|
||||||
|
message: data?.message,
|
||||||
|
code: data?.code,
|
||||||
|
requestData: request.options.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status >= 500 && res.status < 600) {
|
||||||
|
if (request.retries === this.manager.retryLimit) {
|
||||||
|
throw new HTTPError(
|
||||||
|
res.statusText,
|
||||||
|
res.constructor.name,
|
||||||
|
res.status,
|
||||||
|
request.method,
|
||||||
|
request.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.retries++
|
||||||
|
return await this.execute(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
1294
src/rest/endpoints.ts
Normal file
1294
src/rest/endpoints.ts
Normal file
File diff suppressed because it is too large
Load diff
44
src/rest/error.ts
Normal file
44
src/rest/error.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { simplifyAPIError } from '../utils/err_fmt.ts'
|
||||||
|
import { DiscordAPIErrorPayload } from './types.ts'
|
||||||
|
|
||||||
|
export class DiscordAPIError extends Error {
|
||||||
|
name = 'DiscordAPIError'
|
||||||
|
error?: DiscordAPIErrorPayload
|
||||||
|
|
||||||
|
constructor(error: string | DiscordAPIErrorPayload) {
|
||||||
|
super()
|
||||||
|
const fmt = Object.entries(
|
||||||
|
typeof error === 'object' ? simplifyAPIError(error.errors ?? {}) : {}
|
||||||
|
)
|
||||||
|
this.message =
|
||||||
|
typeof error === 'string'
|
||||||
|
? `${error} `
|
||||||
|
: `\n${error.method.toUpperCase()} ${error.url.slice(7)} returned ${
|
||||||
|
error.status
|
||||||
|
}\n(${error.code ?? 'unknown'}) ${error.message}${
|
||||||
|
fmt.length === 0
|
||||||
|
? ''
|
||||||
|
: `\n${fmt
|
||||||
|
.map(
|
||||||
|
(e) =>
|
||||||
|
` at ${e[0]}:\n${e[1]
|
||||||
|
.map((e) => ` - ${e}`)
|
||||||
|
.join('\n')}`
|
||||||
|
)
|
||||||
|
.join('\n')}\n`
|
||||||
|
}`
|
||||||
|
if (typeof error === 'object') this.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HTTPError extends Error {
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
public name: string,
|
||||||
|
public code: number,
|
||||||
|
public method: string,
|
||||||
|
public path: string
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
302
src/rest/manager.ts
Normal file
302
src/rest/manager.ts
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
import { Collection } from '../utils/collection.ts'
|
||||||
|
import type { Client } from '../client/mod.ts'
|
||||||
|
import { RequestMethods, METHODS } from './types.ts'
|
||||||
|
import { Constants } from '../types/constants.ts'
|
||||||
|
import { RESTEndpoints } from './endpoints.ts'
|
||||||
|
import { BucketHandler } from './bucket.ts'
|
||||||
|
import { APIRequest, RequestOptions } from './request.ts'
|
||||||
|
|
||||||
|
export type MethodFunction = (
|
||||||
|
body?: unknown,
|
||||||
|
maxRetries?: number,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
|
) => Promise<any>
|
||||||
|
|
||||||
|
export interface APIMap extends MethodFunction {
|
||||||
|
/** Make a GET request to current route */
|
||||||
|
get: APIMap
|
||||||
|
/** Make a POST request to current route */
|
||||||
|
post: APIMap
|
||||||
|
/** Make a PATCH request to current route */
|
||||||
|
patch: APIMap
|
||||||
|
/** Make a PUT request to current route */
|
||||||
|
put: APIMap
|
||||||
|
/** Make a DELETE request to current route */
|
||||||
|
delete: APIMap
|
||||||
|
/** Make a HEAD request to current route */
|
||||||
|
head: APIMap
|
||||||
|
/** Continue building API Route */
|
||||||
|
[name: string]: APIMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API Route builder function */
|
||||||
|
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)) === true) {
|
||||||
|
const method = ((rest as unknown) as {
|
||||||
|
[name: string]: MethodFunction
|
||||||
|
})[String(p)]
|
||||||
|
return async (...args: any[]) =>
|
||||||
|
await method.bind(rest)(
|
||||||
|
`${Constants.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 to use for authorization */
|
||||||
|
token?: string | (() => string | undefined)
|
||||||
|
/** Headers to patch with if any */
|
||||||
|
headers?: { [name: string]: string | undefined }
|
||||||
|
/** Whether to use Canary instance of Discord API or not */
|
||||||
|
canary?: boolean
|
||||||
|
/** Discord REST API version to use */
|
||||||
|
version?: 6 | 7 | 8
|
||||||
|
/** Token Type to use for Authorization */
|
||||||
|
tokenType?: TokenType
|
||||||
|
/** User Agent to use (Header) */
|
||||||
|
userAgent?: string
|
||||||
|
/** Optional Harmony client */
|
||||||
|
client?: Client
|
||||||
|
/** Requests Timeout (in MS, default 30s) */
|
||||||
|
requestTimeout?: number
|
||||||
|
/** Retry Limit (default 1) */
|
||||||
|
retryLimit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Token Type for REST API. */
|
||||||
|
export enum TokenType {
|
||||||
|
/** Token type for Bot User */
|
||||||
|
Bot = 'Bot',
|
||||||
|
/** Token Type for OAuth2 */
|
||||||
|
Bearer = 'Bearer',
|
||||||
|
/** No Token Type. Can be used for User accounts. */
|
||||||
|
None = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An easier to use interface for interacting with Discord REST API. */
|
||||||
|
export class RESTManager {
|
||||||
|
/** API Version being used by REST Manager */
|
||||||
|
version: number = 8
|
||||||
|
/**
|
||||||
|
* API Map - easy to use way for interacting with Discord API.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* * ```ts
|
||||||
|
* rest.api.users['123'].get().then(userPayload => doSomething)
|
||||||
|
* ```
|
||||||
|
* * ```ts
|
||||||
|
* rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {})
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
api: APIMap
|
||||||
|
/** Token being used for Authorization */
|
||||||
|
token?: string | (() => string | undefined)
|
||||||
|
/** Token Type of the Token if any */
|
||||||
|
tokenType: TokenType = TokenType.Bot
|
||||||
|
/** Headers object which patch the current ones */
|
||||||
|
headers: any = {}
|
||||||
|
/** Optional custom User Agent (header) */
|
||||||
|
userAgent?: string
|
||||||
|
/** Whether REST Manager is using Canary API */
|
||||||
|
canary?: boolean
|
||||||
|
/** Optional Harmony Client object */
|
||||||
|
client?: Client
|
||||||
|
endpoints: RESTEndpoints
|
||||||
|
requestTimeout = 30000
|
||||||
|
timers: Set<number> = new Set()
|
||||||
|
apiURL = Constants.DISCORD_API_URL
|
||||||
|
|
||||||
|
handlers = new Collection<string, BucketHandler>()
|
||||||
|
globalLimit = Infinity
|
||||||
|
globalRemaining = this.globalLimit
|
||||||
|
globalReset: number | null = null
|
||||||
|
globalDelay: number | null = null
|
||||||
|
retryLimit = 1
|
||||||
|
restTimeOffset = 0
|
||||||
|
|
||||||
|
constructor(options?: RESTOptions) {
|
||||||
|
this.api = builder(this)
|
||||||
|
if (options?.token !== undefined) this.token = options.token
|
||||||
|
if (options?.version !== undefined) this.version = options.version
|
||||||
|
if (options?.headers !== undefined) this.headers = options.headers
|
||||||
|
if (options?.tokenType !== undefined) this.tokenType = options.tokenType
|
||||||
|
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
|
||||||
|
if (options?.canary !== undefined) this.canary = options.canary
|
||||||
|
if (options?.client !== undefined) this.client = options.client
|
||||||
|
if (options?.retryLimit !== undefined) this.retryLimit = options.retryLimit
|
||||||
|
if (options?.requestTimeout !== undefined)
|
||||||
|
this.requestTimeout = options.requestTimeout
|
||||||
|
this.endpoints = new RESTEndpoints(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(fn: (...args: any[]) => any, ms: number): number {
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
this.timers.delete(timer)
|
||||||
|
await fn()
|
||||||
|
}, ms)
|
||||||
|
this.timers.add(timer)
|
||||||
|
return timer
|
||||||
|
}
|
||||||
|
|
||||||
|
async request<T = any>(
|
||||||
|
method: RequestMethods,
|
||||||
|
path: string,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const req = new APIRequest(this, method, path, options)
|
||||||
|
let handler = this.handlers.get(req.path)
|
||||||
|
|
||||||
|
if (handler === undefined) {
|
||||||
|
handler = new BucketHandler(this)
|
||||||
|
this.handlers.set(req.route, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.push(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a Request to Discord API.
|
||||||
|
* @param method HTTP Method to use
|
||||||
|
* @param url URL of the Request
|
||||||
|
* @param body Body to send with Request
|
||||||
|
* @param maxRetries Number of Max Retries to perform
|
||||||
|
* @param bucket BucketID of the Request
|
||||||
|
* @param rawResponse Whether to get Raw Response or body itself
|
||||||
|
*/
|
||||||
|
async make(
|
||||||
|
method: RequestMethods,
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
_maxRetries = 0,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.request(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
data: body,
|
||||||
|
rawResponse,
|
||||||
|
route: bucket ?? undefined
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Makes a GET Request to API */
|
||||||
|
async get(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
maxRetries = 0,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.make(
|
||||||
|
'get',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Makes a POST Request to API */
|
||||||
|
async post(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
maxRetries = 0,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.make(
|
||||||
|
'post',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Makes a DELETE Request to API */
|
||||||
|
async delete(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
maxRetries = 0,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.make(
|
||||||
|
'delete',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Makes a PATCH Request to API */
|
||||||
|
async patch(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
maxRetries = 0,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.make(
|
||||||
|
'patch',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Makes a PUT Request to API */
|
||||||
|
async put(
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
maxRetries = 0,
|
||||||
|
bucket?: string | null,
|
||||||
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.make(
|
||||||
|
'put',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
7
src/rest/mod.ts
Normal file
7
src/rest/mod.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './manager.ts'
|
||||||
|
export * from './types.ts'
|
||||||
|
export * from './endpoints.ts'
|
||||||
|
export * from './error.ts'
|
||||||
|
export * from './bucket.ts'
|
||||||
|
export * from './queue.ts'
|
||||||
|
export * from './request.ts'
|
37
src/rest/queue.ts
Normal file
37
src/rest/queue.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// based on https://github.com/discordjs/discord.js/blob/master/src/rest/AsyncQueue.js
|
||||||
|
|
||||||
|
export interface RequestPromise {
|
||||||
|
resolve: CallableFunction
|
||||||
|
promise: Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestQueue {
|
||||||
|
promises: RequestPromise[] = []
|
||||||
|
|
||||||
|
get remaining(): number {
|
||||||
|
return this.promises.length
|
||||||
|
}
|
||||||
|
|
||||||
|
async wait(): Promise<any> {
|
||||||
|
const next =
|
||||||
|
this.promises.length !== 0
|
||||||
|
? this.promises[this.promises.length - 1].promise
|
||||||
|
: Promise.resolve()
|
||||||
|
let resolveFn: CallableFunction | undefined
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
resolveFn = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
this.promises.push({
|
||||||
|
resolve: resolveFn!,
|
||||||
|
promise
|
||||||
|
})
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
shift(): void {
|
||||||
|
const deferred = this.promises.shift()
|
||||||
|
if (typeof deferred !== 'undefined') deferred.resolve()
|
||||||
|
}
|
||||||
|
}
|
132
src/rest/request.ts
Normal file
132
src/rest/request.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import type { Embed } from '../structures/embed.ts'
|
||||||
|
import type { MessageAttachment } from '../structures/message.ts'
|
||||||
|
import type { RESTManager } from './manager.ts'
|
||||||
|
import type { RequestMethods } from './types.ts'
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
headers?: { [name: string]: string }
|
||||||
|
query?: { [name: string]: string }
|
||||||
|
files?: MessageAttachment[]
|
||||||
|
data?: any
|
||||||
|
reason?: string
|
||||||
|
rawResponse?: boolean
|
||||||
|
route?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIRequest {
|
||||||
|
retries = 0
|
||||||
|
route: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public rest: RESTManager,
|
||||||
|
public method: RequestMethods,
|
||||||
|
public path: string,
|
||||||
|
public options: RequestOptions
|
||||||
|
) {
|
||||||
|
this.route = options.route ?? path
|
||||||
|
if (typeof options.query === 'object') {
|
||||||
|
const entries = Object.entries(options.query)
|
||||||
|
if (entries.length > 0) {
|
||||||
|
this.path += '?'
|
||||||
|
entries.forEach((entry, i) => {
|
||||||
|
this.path += `${i === 0 ? '' : '&'}${encodeURIComponent(
|
||||||
|
entry[0]
|
||||||
|
)}=${encodeURIComponent(entry[1])}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _files: undefined | MessageAttachment[]
|
||||||
|
if (
|
||||||
|
options.data?.embed?.files !== undefined &&
|
||||||
|
Array.isArray(options.data?.embed?.files)
|
||||||
|
) {
|
||||||
|
_files = [...options.data?.embed?.files]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.data?.embeds !== undefined &&
|
||||||
|
Array.isArray(options.data?.embeds)
|
||||||
|
) {
|
||||||
|
const files1 = options.data?.embeds
|
||||||
|
.map((e: Embed) => e.files)
|
||||||
|
.filter((e: MessageAttachment[]) => e !== undefined)
|
||||||
|
for (const files of files1) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (_files === undefined) _files = []
|
||||||
|
_files?.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.data?.file !== undefined) {
|
||||||
|
if (_files === undefined) _files = []
|
||||||
|
_files.push(options.data?.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.data?.files !== undefined &&
|
||||||
|
Array.isArray(options.data?.files)
|
||||||
|
) {
|
||||||
|
if (_files === undefined) _files = []
|
||||||
|
options.data?.files.forEach((file: any) => {
|
||||||
|
_files!.push(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_files !== undefined && _files.length > 0) {
|
||||||
|
if (options.files === undefined) options.files = _files
|
||||||
|
else options.files = [...options.files, ..._files]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(): Promise<Response> {
|
||||||
|
let contentType: string | undefined
|
||||||
|
let body: any = this.options.data
|
||||||
|
if (this.options.files !== undefined && this.options.files.length > 0) {
|
||||||
|
contentType = undefined
|
||||||
|
const form = new FormData()
|
||||||
|
this.options.files.forEach((file, i) =>
|
||||||
|
form.append(`file${i === 0 ? '' : i}`, file.blob, file.name)
|
||||||
|
)
|
||||||
|
form.append('payload_json', JSON.stringify(body))
|
||||||
|
body = form
|
||||||
|
} else {
|
||||||
|
contentType = 'application/json'
|
||||||
|
body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
controller.abort()
|
||||||
|
}, this.rest.requestTimeout)
|
||||||
|
this.rest.timers.add(timer)
|
||||||
|
|
||||||
|
const url = this.path.startsWith('http')
|
||||||
|
? this.path
|
||||||
|
: `${this.rest.apiURL}/v${this.rest.version}${this.path}`
|
||||||
|
|
||||||
|
const headers: any = {
|
||||||
|
'User-Agent':
|
||||||
|
this.rest.userAgent ??
|
||||||
|
`DiscordBot (harmony, https://github.com/harmonyland/harmony)`,
|
||||||
|
Authorization:
|
||||||
|
this.rest.token === undefined
|
||||||
|
? undefined
|
||||||
|
: `${this.rest.tokenType} ${this.rest.token}`.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType !== undefined) headers['Content-Type'] = contentType
|
||||||
|
|
||||||
|
const init: RequestInit = {
|
||||||
|
method: this.method.toUpperCase(),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: Object.assign(headers, this.rest.headers, this.options.headers),
|
||||||
|
body
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, init).finally(() => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
this.rest.timers.delete(timer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
37
src/rest/types.ts
Normal file
37
src/rest/types.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
export type RequestMethods =
|
||||||
|
| 'get'
|
||||||
|
| 'post'
|
||||||
|
| 'put'
|
||||||
|
| 'patch'
|
||||||
|
| 'head'
|
||||||
|
| 'delete'
|
||||||
|
|
||||||
|
export enum HttpResponseCode {
|
||||||
|
Ok = 200,
|
||||||
|
Created = 201,
|
||||||
|
NoContent = 204,
|
||||||
|
NotModified = 304,
|
||||||
|
BadRequest = 400,
|
||||||
|
Unauthorized = 401,
|
||||||
|
Forbidden = 403,
|
||||||
|
NotFound = 404,
|
||||||
|
MethodNotAllowed = 405,
|
||||||
|
TooManyRequests = 429,
|
||||||
|
GatewayUnavailable = 502
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestHeaders {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscordAPIErrorPayload {
|
||||||
|
url: string
|
||||||
|
status: number
|
||||||
|
method: string
|
||||||
|
code?: number
|
||||||
|
message?: string
|
||||||
|
errors: object
|
||||||
|
requestData: { [key: string]: any }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const METHODS = ['get', 'post', 'patch', 'put', 'delete', 'head']
|
|
@ -1,5 +1,5 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { ApplicationPayload } from '../types/application.ts'
|
import type { ApplicationPayload } from '../types/application.ts'
|
||||||
import { SnowflakeBase } from './base.ts'
|
import { SnowflakeBase } from './base.ts'
|
||||||
import { User } from './user.ts'
|
import { User } from './user.ts'
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { Snowflake } from '../utils/snowflake.ts'
|
import { Snowflake } from '../utils/snowflake.ts'
|
||||||
|
|
||||||
export class Base {
|
export class Base {
|
||||||
client: Client
|
client!: Client
|
||||||
|
|
||||||
constructor(client: Client, _data?: any) {
|
constructor(client: Client, _data?: any) {
|
||||||
this.client = client
|
Object.defineProperty(this, 'client', { value: client, enumerable: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ImageFormats, ImageSize } from '../types/cdn.ts'
|
import type { ImageFormats, ImageSize } from '../types/cdn.ts'
|
||||||
|
|
||||||
/** Function to get Image URL from a resource on Discord CDN */
|
/** Function to get Image URL from a resource on Discord CDN */
|
||||||
export const ImageURL = (
|
export const ImageURL = (
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import {
|
import type {
|
||||||
ChannelPayload,
|
ChannelPayload,
|
||||||
ChannelTypes,
|
ChannelTypes,
|
||||||
ModifyChannelOption,
|
ModifyChannelOption,
|
||||||
ModifyChannelPayload,
|
ModifyChannelPayload,
|
||||||
Overwrite,
|
Overwrite,
|
||||||
OverwritePayload,
|
OverwritePayload,
|
||||||
OverwriteAsArg,
|
OverwriteAsArg
|
||||||
OverrideType
|
|
||||||
} from '../types/channel.ts'
|
} from '../types/channel.ts'
|
||||||
|
import { OverrideType } from '../types/channel.ts'
|
||||||
import { CHANNEL } from '../types/endpoint.ts'
|
import { CHANNEL } from '../types/endpoint.ts'
|
||||||
import { GuildChannelPayloads, GuildChannels } from '../types/guild.ts'
|
import type { GuildChannelPayloads, GuildChannels } from '../types/guild.ts'
|
||||||
import getChannelByType from '../utils/getChannelByType.ts'
|
import getChannelByType from '../utils/channel.ts'
|
||||||
import { Permissions } from '../utils/permissions.ts'
|
import { Permissions } from '../utils/permissions.ts'
|
||||||
import { SnowflakeBase } from './base.ts'
|
import { SnowflakeBase } from './base.ts'
|
||||||
import { Guild } from './guild.ts'
|
import type { Guild } from './guild.ts'
|
||||||
import { Member } from './member.ts'
|
import { Member } from './member.ts'
|
||||||
import { Role } from './role.ts'
|
import { Role } from './role.ts'
|
||||||
|
|
||||||
|
@ -303,7 +303,10 @@ export class GuildChannel extends Channel {
|
||||||
: overwrite.allow?.toJSON() ?? overwrites[index].allow
|
: overwrite.allow?.toJSON() ?? overwrites[index].allow
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overwrite.deny !== undefined && overwriteDeny !== OverrideType.REPLACE) {
|
if (
|
||||||
|
overwrite.deny !== undefined &&
|
||||||
|
overwriteDeny !== OverrideType.REPLACE
|
||||||
|
) {
|
||||||
switch (overwriteDeny) {
|
switch (overwriteDeny) {
|
||||||
case OverrideType.ADD: {
|
case OverrideType.ADD: {
|
||||||
const originalDeny = new Permissions(overwrites[index].deny)
|
const originalDeny = new Permissions(overwrites[index].deny)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Client } from '../models/client.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { DMChannelPayload } from '../types/channel.ts'
|
import type { DMChannelPayload } from '../types/channel.ts'
|
||||||
import { UserPayload } from '../types/user.ts'
|
import type { UserPayload } from '../types/user.ts'
|
||||||
import { TextChannel } from './textChannel.ts'
|
import { TextChannel } from './textChannel.ts'
|
||||||
|
|
||||||
export class DMChannel extends TextChannel {
|
export class DMChannel extends TextChannel {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {
|
import type {
|
||||||
EmbedAuthor,
|
EmbedAuthor,
|
||||||
EmbedField,
|
EmbedField,
|
||||||
EmbedFooter,
|
EmbedFooter,
|
||||||
|
@ -10,7 +10,7 @@ import {
|
||||||
EmbedVideo
|
EmbedVideo
|
||||||
} from '../types/channel.ts'
|
} from '../types/channel.ts'
|
||||||
import { Colors, ColorUtil } from '../utils/colorutil.ts'
|
import { Colors, ColorUtil } from '../utils/colorutil.ts'
|
||||||
import { MessageAttachment } from './message.ts'
|
import type { MessageAttachment } from './message.ts'
|
||||||
|
|
||||||
/** Message Embed Object */
|
/** Message Embed Object */
|
||||||
export class Embed {
|
export class Embed {
|
||||||
|
@ -56,43 +56,71 @@ export class Embed {
|
||||||
|
|
||||||
/** Convert Embed Object to Embed Payload JSON */
|
/** Convert Embed Object to Embed Payload JSON */
|
||||||
toJSON(): EmbedPayload {
|
toJSON(): EmbedPayload {
|
||||||
let total = 0;
|
let total = 0
|
||||||
if (this.title?.length !== undefined && this.title?.length > Embed.MAX_TITLE_LENGTH) {
|
if (
|
||||||
|
this.title?.length !== undefined &&
|
||||||
|
this.title?.length > Embed.MAX_TITLE_LENGTH
|
||||||
|
) {
|
||||||
total += Number(this.title.length)
|
total += Number(this.title.length)
|
||||||
throw new Error(`Embed title cannot exceed ${Embed.MAX_TITLE_LENGTH} characters.`)
|
throw new Error(
|
||||||
|
`Embed title cannot exceed ${Embed.MAX_TITLE_LENGTH} characters.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.description?.length !== undefined && this.description?.length > Embed.MAX_DESCRIPTION_LENGTH) {
|
if (
|
||||||
|
this.description?.length !== undefined &&
|
||||||
|
this.description?.length > Embed.MAX_DESCRIPTION_LENGTH
|
||||||
|
) {
|
||||||
total += Number(this.description.length)
|
total += Number(this.description.length)
|
||||||
throw new Error(`Embed description cannot exceed ${Embed.MAX_DESCRIPTION_LENGTH} characters.`)
|
throw new Error(
|
||||||
|
`Embed description cannot exceed ${Embed.MAX_DESCRIPTION_LENGTH} characters.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fields?.length !== undefined) {
|
if (this.fields?.length !== undefined) {
|
||||||
this.fields.forEach((field) => {
|
this.fields.forEach((field) => {
|
||||||
if (field.name.length > Embed.MAX_FIELD_NAME_LENGTH) {
|
if (field.name.length > Embed.MAX_FIELD_NAME_LENGTH) {
|
||||||
total += Number(field.name.length)
|
total += Number(field.name.length)
|
||||||
throw new Error(`Embed field name cannot exceed ${Embed.MAX_FIELD_NAME_LENGTH} characters.`)
|
throw new Error(
|
||||||
|
`Embed field name cannot exceed ${Embed.MAX_FIELD_NAME_LENGTH} characters.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.value.length > Embed.MAX_FIELD_VALUE_LENGTH) {
|
if (field.value.length > Embed.MAX_FIELD_VALUE_LENGTH) {
|
||||||
total += Number(field.value.length)
|
total += Number(field.value.length)
|
||||||
throw new Error(`Embed field value cannot exceed ${Embed.MAX_FIELD_VALUE_LENGTH} characters.`)
|
throw new Error(
|
||||||
|
`Embed field value cannot exceed ${Embed.MAX_FIELD_VALUE_LENGTH} characters.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (this.fields.length > Embed.MAX_FIELDS_LENGTH) throw new Error('Embed fields cannot exceed 25 field objects.')
|
if (this.fields.length > Embed.MAX_FIELDS_LENGTH)
|
||||||
|
throw new Error('Embed fields cannot exceed 25 field objects.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.footer?.text?.length !== undefined && this.footer?.text?.length > Embed.MAX_FOOTER_TEXT_LENGTH) {
|
if (
|
||||||
|
this.footer?.text?.length !== undefined &&
|
||||||
|
this.footer?.text?.length > Embed.MAX_FOOTER_TEXT_LENGTH
|
||||||
|
) {
|
||||||
total += Number(this.footer?.text?.length)
|
total += Number(this.footer?.text?.length)
|
||||||
throw new Error(`Embed footer text cannot exceed ${Embed.MAX_FOOTER_TEXT_LENGTH}.`)
|
throw new Error(
|
||||||
|
`Embed footer text cannot exceed ${Embed.MAX_FOOTER_TEXT_LENGTH}.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.author?.name?.length !== undefined && this.author?.name?.length > Embed.MAX_AUTHOR_NAME_LENGTH) {
|
if (
|
||||||
|
this.author?.name?.length !== undefined &&
|
||||||
|
this.author?.name?.length > Embed.MAX_AUTHOR_NAME_LENGTH
|
||||||
|
) {
|
||||||
total += Number(this.author?.name?.length)
|
total += Number(this.author?.name?.length)
|
||||||
throw new Error(`Embed author name cannot exceed ${Embed.MAX_AUTHOR_NAME_LENGTH}.`)
|
throw new Error(
|
||||||
|
`Embed author name cannot exceed ${Embed.MAX_AUTHOR_NAME_LENGTH}.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total > Embed.MAX_EMBED_LENGTH) throw new Error(`Embed characters cannot exceed ${Embed.MAX_EMBED_LENGTH} characters in total.`)
|
if (total > Embed.MAX_EMBED_LENGTH)
|
||||||
|
throw new Error(
|
||||||
|
`Embed characters cannot exceed ${Embed.MAX_EMBED_LENGTH} characters in total.`
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue