This commit is contained in:
DjDeveloperr 2021-04-23 11:08:51 +05:30
commit a1ead7e15e
169 changed files with 4849 additions and 2446 deletions

View File

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

3
.gitignore vendored
View File

@ -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
@ -117,4 +118,4 @@ src/test/config.ts
# Webstorm dont forget this duude :) # Webstorm dont forget this duude :)
.idea/ .idea/
src/test/music.mp3 src/test/music.mp3

View File

@ -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
View 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'

View File

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

73
mod.ts
View File

@ -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
View 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
View 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
View File

@ -0,0 +1,4 @@
export * from './adapter.ts'
export * from './default.ts'
// Not exported by default
// export * from './redis.ts'

View File

@ -1,176 +1,110 @@
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 { /** Redis Cache Adapter for using Redis as a cache-provider. */
/** Gets a key from a Cache */ export class RedisCacheAdapter implements ICacheAdapter {
get: (cacheName: string, key: string) => Promise<any> | any _redis: Promise<Redis>
/** Sets a key to value in a Cache Name with optional expire value in MS */ redis?: Redis
set: ( ready: boolean = false
cacheName: string, readonly _expireIntervalTimer: number = 5000
key: string, private _expireInterval?: number
value: any,
expire?: number constructor(options: RedisConnectOptions) {
) => Promise<any> | any this._redis = connect(options)
/** Deletes a key from a Cache */ this._redis.then(
delete: (cacheName: string, key: string) => Promise<boolean> | boolean (redis) => {
/** Gets array of all values in a Cache */ this.redis = redis
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> this.ready = true
/** Entirely deletes a Cache */ this._startExpireInterval()
deleteCache: (cacheName: string) => any },
} () => {
// TODO: Make error for this
/** Default Cache Adapter for in-memory caching. */ }
export class DefaultCacheAdapter implements ICacheAdapter { )
data: { }
[name: string]: Collection<string, any>
} = {} private _startExpireInterval(): void {
this._expireInterval = setInterval(() => {
async get(cacheName: string, key: string): Promise<undefined | any> { this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => {
const cache = this.data[cacheName] for (const name of names) {
if (cache === undefined) return this.redis?.hvals(name).then((vals) => {
return cache.get(key) for (const val of vals) {
} const expireVal: {
name: string
async set( key: string
cacheName: string, at: number
key: string, } = JSON.parse(val)
value: any, const expired = new Date().getTime() > expireVal.at
expire?: number if (expired) this.redis?.hdel(expireVal.name, expireVal.key)
): Promise<any> { }
let cache = this.data[cacheName] })
if (cache === undefined) { }
this.data[cacheName] = new Collection() })
cache = this.data[cacheName] }, this._expireIntervalTimer)
} }
cache.set(key, value)
if (expire !== undefined) async _checkReady(): Promise<void> {
setTimeout(() => { if (!this.ready) await this._redis
cache.delete(key) }
}, expire)
} async get(cacheName: string, key: string): Promise<string | undefined> {
await this._checkReady()
async delete(cacheName: string, key: string): Promise<boolean> { const cache = await this.redis?.hget(cacheName, key)
const cache = this.data[cacheName] if (cache === undefined) return
if (cache === undefined) return false try {
return cache.delete(key) return JSON.parse(cache)
} } catch (e) {
return cache
async array(cacheName: string): Promise<any[] | undefined> { }
const cache = this.data[cacheName] }
if (cache === undefined) return
return cache.array() async set(
} cacheName: string,
key: string,
async deleteCache(cacheName: string): Promise<boolean> { value: any,
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete expire?: number
return delete this.data[cacheName] ): Promise<number | undefined> {
} await this._checkReady()
} const result = await this.redis?.hset(
cacheName,
/** Redis Cache Adapter for using Redis as a cache-provider. */ key,
export class RedisCacheAdapter implements ICacheAdapter { typeof value === 'object' ? JSON.stringify(value) : value
_redis: Promise<Redis> )
redis?: Redis if (expire !== undefined) {
ready: boolean = false await this.redis?.hset(
readonly _expireIntervalTimer: number = 5000 `${cacheName}:expires`,
private _expireInterval?: number key,
JSON.stringify({
constructor(options: RedisConnectOptions) { name: cacheName,
this._redis = connect(options) key,
this._redis.then( at: new Date().getTime() + expire
(redis) => { })
this.redis = redis )
this.ready = true }
this._startExpireInterval() return result
}, }
() => {
// TODO: Make error for this async delete(cacheName: string, key: string): Promise<boolean> {
} await this._checkReady()
) const exists = await this.redis?.hexists(cacheName, key)
} if (exists === 0) return false
await this.redis?.hdel(cacheName, key)
private _startExpireInterval(): void { return true
this._expireInterval = setInterval(() => { }
this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => {
for (const name of names) { async array(cacheName: string): Promise<any[] | undefined> {
this.redis?.hvals(name).then((vals) => { await this._checkReady()
for (const val of vals) { const data = await this.redis?.hvals(cacheName)
const expireVal: { return data?.map((e: string) => JSON.parse(e))
name: string }
key: string
at: number async deleteCache(cacheName: string): Promise<boolean> {
} = JSON.parse(val) await this._checkReady()
const expired = new Date().getTime() > expireVal.at return (await this.redis?.del(cacheName)) !== 0
if (expired) this.redis?.hdel(expireVal.name, expireVal.key) }
} }
})
}
})
}, this._expireIntervalTimer)
}
async _checkReady(): Promise<void> {
if (!this.ready) await this._redis
}
async get(cacheName: string, key: string): Promise<string | undefined> {
await this._checkReady()
const cache = await this.redis?.hget(cacheName, key)
if (cache === undefined) return
try {
return JSON.parse(cache)
} catch (e) {
return cache
}
}
async set(
cacheName: string,
key: string,
value: any,
expire?: number
): Promise<number | undefined> {
await this._checkReady()
const result = await this.redis?.hset(
cacheName,
key,
typeof value === 'object' ? JSON.stringify(value) : value
)
if (expire !== undefined) {
await this.redis?.hset(
`${cacheName}:expires`,
key,
JSON.stringify({
name: cacheName,
key,
at: new Date().getTime() + expire
})
)
}
return result
}
async delete(cacheName: string, key: string): Promise<boolean> {
await this._checkReady()
const exists = await this.redis?.hexists(cacheName, key)
if (exists === 0) return false
await this.redis?.hdel(cacheName, key)
return true
}
async array(cacheName: string): Promise<any[] | undefined> {
await this._checkReady()
const data = await this.redis?.hvals(cacheName)
return data?.map((e: string) => JSON.parse(e))
}
async deleteCache(cacheName: string): Promise<boolean> {
await this._checkReady()
return (await this.redis?.del(cacheName)) !== 0
}
}

View File

@ -1,494 +1,440 @@
/* 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 type { ClientEvents } from '../gateway/handlers/mod.ts'
import { ClientEvents } from '../gateway/handlers/index.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 type { VoiceRegion } from '../types/voice.ts'
import { VoiceRegion } from '../types/voice.ts' import { fetchAuto } from '../../deps.ts'
import { fetchAuto } from '../../deps.ts' import type { DMChannel } from '../structures/dmChannel.ts'
import { 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 */ export interface ClientProperties {
export interface ClientProperties { os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string
os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string browser?: 'harmony' | string
browser?: 'harmony' | string device?: 'harmony' | string
device?: 'harmony' | string }
}
/** Some Client Options to modify behaviour */
/** Some Client Options to modify behaviour */ export interface ClientOptions {
export interface ClientOptions { /** ID of the Client/Application to initialize Slash Client REST */
/** ID of the Client/Application to initialize Slash Client REST */ id?: string
id?: string /** Token of the Bot/User */
/** Token of the Bot/User */ token?: string
token?: string /** Gateway Intents */
/** Gateway Intents */ intents?: GatewayIntents[]
intents?: GatewayIntents[] /** Cache Adapter to use, defaults to Collections one */
/** Cache Adapter to use, defaults to Collections one */ cache?: ICacheAdapter
cache?: ICacheAdapter /** Force New Session and don't use cached Session (by persistent caching) */
/** Force New Session and don't use cached Session (by persistent caching) */ forceNewSession?: boolean
forceNewSession?: boolean /** Startup presence of client */
/** Startup presence of client */ presence?: ClientPresence | ClientActivity | ActivityGame
presence?: ClientPresence | ClientActivity | ActivityGame /** Force all requests to Canary API */
/** Force all requests to Canary API */ canary?: boolean
canary?: boolean /** Time till which Messages are to be cached, in MS. Default is 3600000 */
/** Time till which Messages are to be cached, in MS. Default is 3600000 */ messageCacheLifetime?: number
messageCacheLifetime?: number /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */
/** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ reactionCacheLifetime?: number
reactionCacheLifetime?: number /** Whether to fetch Uncached Message of Reaction or not? */
/** Whether to fetch Uncached Message of Reaction or not? */ fetchUncachedReactions?: boolean
fetchUncachedReactions?: boolean /** Client Properties */
/** Client Properties */ clientProperties?: ClientProperties
clientProperties?: ClientProperties /** Enable/Disable Slash Commands Integration (enabled by default) */
/** Enable/Disable Slash Commands Integration (enabled by default) */ enableSlash?: boolean
enableSlash?: boolean /** Disable taking token from env if not provided (token is taken from env if present by default) */
/** Disable taking token from env if not provided (token is taken from env if present by default) */ disableEnvToken?: boolean
disableEnvToken?: boolean /** Override REST Options */
/** Override REST Options */ restOptions?: RESTOptions
restOptions?: RESTOptions /** Whether to fetch Gateway info or not */
/** Whether to fetch Gateway info or not */ fetchGatewayInfo?: boolean
fetchGatewayInfo?: boolean /** ADVANCED: Shard ID to launch on */
/** ADVANCED: Shard ID to launch on */ shard?: number
shard?: number /** ADVACNED: Shard count. */
/** ADVACNED: Shard count. */ shardCount?: number | 'auto'
shardCount?: number | 'auto' }
}
/**
/** * Harmony Client. Provides high-level interface over the REST and WebSocket API.
* Discord Client. */
*/ 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 /** Token of the Bot/User */
/** WebSocket ping of Client */ token?: string
ping = 0 /** Cache Adapter */
/** Token of the Bot/User */ cache: ICacheAdapter = new DefaultCacheAdapter()
token?: string /** Gateway Intents */
/** Cache Adapter */ intents?: GatewayIntents[]
cache: ICacheAdapter = new DefaultCacheAdapter() /** Whether to force new session or not */
/** Gateway Intents */ forceNewSession?: boolean
intents?: GatewayIntents[] /** Time till messages to stay cached, in MS. */
/** Whether to force new session or not */ messageCacheLifetime: number = 3600000
forceNewSession?: boolean /** Time till messages to stay cached, in MS. */
/** Time till messages to stay cached, in MS. */ reactionCacheLifetime: number = 3600000
messageCacheLifetime: number = 3600000 /** Whether to fetch Uncached Message of Reaction or not? */
/** Time till messages to stay cached, in MS. */ fetchUncachedReactions: boolean = false
reactionCacheLifetime: number = 3600000 /** Client Properties */
/** Whether to fetch Uncached Message of Reaction or not? */ clientProperties: ClientProperties
fetchUncachedReactions: boolean = false /** Slash-Commands Management client */
/** Client Properties */ slash: SlashClient
clientProperties: ClientProperties /** Whether to fetch Gateway info or not */
/** Slash-Commands Management client */ fetchGatewayInfo: boolean = true
slash: SlashClient
/** Whether to fetch Gateway info or not */ /** Users Manager, containing all Users cached */
fetchGatewayInfo: boolean = true users: UsersManager = new UsersManager(this)
/** Guilds Manager, providing cache & API interface to Guilds */
/** Users Manager, containing all Users cached */ guilds: GuildManager = new GuildManager(this)
users: UsersManager = new UsersManager(this) /** Channels Manager, providing cache interface to Channels */
/** Guilds Manager, providing cache & API interface to Guilds */ channels: ChannelsManager = new ChannelsManager(this)
guilds: GuildManager = new GuildManager(this) /** Channels Manager, providing cache interface to Channels */
/** Channels Manager, providing cache interface to Channels */ emojis: EmojisManager = new EmojisManager(this)
channels: ChannelsManager = new ChannelsManager(this)
/** Channels Manager, providing cache interface to Channels */ /** Last READY timestamp */
emojis: EmojisManager = new EmojisManager(this) upSince?: Date
/** Last READY timestamp */ /** Client's presence. Startup one if set before connecting */
upSince?: Date presence: ClientPresence = new ClientPresence()
_decoratedEvents?: {
/** Client's presence. Startup one if set before connecting */ [name: string]: (...args: any[]) => void
presence: ClientPresence = new ClientPresence() }
_decoratedEvents?: {
[name: string]: (...args: any[]) => void _decoratedSlash?: Array<{
} name: string
guild?: string
_decoratedSlash?: Array<{ parent?: string
name: string group?: string
guild?: string handler: (interaction: Interaction) => any
parent?: string }>
group?: string
handler: (interaction: Interaction) => any _id?: string
}>
/** Shard on which this Client is */
_id?: string shard?: number
/** Shard Count */
/** Shard on which this Client is */ shardCount: number | 'auto' = 'auto'
shard?: number /** Shard Manager of this Client if Sharded */
/** Shard Count */ shards: ShardManager
shardCount: number | 'auto' = 'auto' /** Collectors set */
/** Shard Manager of this Client if Sharded */ collectors: Set<Collector> = new Set()
shards: ShardManager
/** Collectors set */ /** Since when is Client online (ready). */
collectors: Set<Collector> = new Set() get uptime(): number {
if (this.upSince === undefined) return 0
/** Since when is Client online (ready). */ else {
get uptime(): number { const dif = Date.now() - this.upSince.getTime()
if (this.upSince === undefined) return 0 if (dif < 0) return 0
else { else return dif
const dif = Date.now() - this.upSince.getTime() }
if (dif < 0) return 0 }
else return dif
} /** Get Shard 0's Gateway */
} get gateway(): Gateway {
return this.shards.list.get('0')!
get gateway(): Gateway { }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return this.shards.list.get('0') as Gateway applicationID?: string
} applicationFlags?: number
applicationID?: string constructor(options: ClientOptions = {}) {
applicationFlags?: number super()
this._id = options.id
constructor(options: ClientOptions = {}) { this.token = options.token
super() this.intents = options.intents
this._id = options.id this.shards = new ShardManager(this)
this.token = options.token this.forceNewSession = options.forceNewSession
this.intents = options.intents if (options.cache !== undefined) this.cache = options.cache
this.shards = new ShardManager(this) if (options.presence !== undefined)
this.forceNewSession = options.forceNewSession this.presence =
if (options.cache !== undefined) this.cache = options.cache options.presence instanceof ClientPresence
if (options.presence !== undefined) ? options.presence
this.presence = : new ClientPresence(options.presence)
options.presence instanceof ClientPresence if (options.messageCacheLifetime !== undefined)
? options.presence this.messageCacheLifetime = options.messageCacheLifetime
: new ClientPresence(options.presence) if (options.reactionCacheLifetime !== undefined)
if (options.messageCacheLifetime !== undefined) this.reactionCacheLifetime = options.reactionCacheLifetime
this.messageCacheLifetime = options.messageCacheLifetime if (options.fetchUncachedReactions === true)
if (options.reactionCacheLifetime !== undefined) this.fetchUncachedReactions = true
this.reactionCacheLifetime = options.reactionCacheLifetime
if (options.fetchUncachedReactions === true) if (
this.fetchUncachedReactions = true this._decoratedEvents !== undefined &&
Object.keys(this._decoratedEvents).length !== 0
if ( ) {
this._decoratedEvents !== undefined && Object.entries(this._decoratedEvents).forEach((entry) => {
Object.keys(this._decoratedEvents).length !== 0 this.on(entry[0] as keyof ClientEvents, entry[1].bind(this))
) { })
Object.entries(this._decoratedEvents).forEach((entry) => { this._decoratedEvents = undefined
this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) }
})
this._decoratedEvents = undefined this.clientProperties =
} options.clientProperties === undefined
? {
this.clientProperties = os: Deno.build.os,
options.clientProperties === undefined browser: 'harmony',
? { device: 'harmony'
os: Deno.build.os, }
browser: 'harmony', : options.clientProperties
device: 'harmony'
} if (options.shard !== undefined) this.shard = options.shard
: options.clientProperties if (options.shardCount !== undefined) this.shardCount = options.shardCount
if (options.shard !== undefined) this.shard = options.shard this.fetchGatewayInfo = options.fetchGatewayInfo ?? true
if (options.shardCount !== undefined) this.shardCount = options.shardCount
if (this.token === undefined) {
this.fetchGatewayInfo = options.fetchGatewayInfo ?? true try {
const token = Deno.env.get('DISCORD_TOKEN')
if (this.token === undefined) { if (token !== undefined) {
try { this.token = token
const token = Deno.env.get('DISCORD_TOKEN') this.debug('Info', 'Found token in ENV')
if (token !== undefined) { }
this.token = token } catch (e) {}
this.debug('Info', 'Found token in ENV') }
}
} catch (e) {} const restOptions: RESTOptions = {
} token: () => this.token,
tokenType: TokenType.Bot,
const restOptions: RESTOptions = { canary: options.canary,
token: () => this.token, client: this
tokenType: TokenType.Bot, }
canary: options.canary,
client: this if (options.restOptions !== undefined)
} Object.assign(restOptions, options.restOptions)
this.rest = new RESTManager(restOptions)
if (options.restOptions !== undefined)
Object.assign(restOptions, options.restOptions) this.slash = new SlashClient({
this.rest = new RESTManager(restOptions) id: () => this.getEstimatedID(),
client: this,
this.slash = new SlashClient({ enabled: options.enableSlash
id: () => this.getEstimatedID(), })
client: this, }
enabled: options.enableSlash
}) /**
} * Sets Cache Adapter
*
/** * Should NOT be set after bot is already logged in or using current cache.
* Sets Cache Adapter * Please look into using `cache` option.
* */
* Should NOT be set after bot is already logged in or using current cache. setAdapter(adapter: ICacheAdapter): Client {
* Please look into using `cache` option. this.cache = adapter
*/ return this
setAdapter(adapter: ICacheAdapter): Client { }
this.cache = adapter
return this /** Changes Presence of Client */
} setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void {
if (presence instanceof ClientPresence) {
/** Changes Presence of Client */ this.presence = presence
setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { } else this.presence = new ClientPresence(presence)
if (presence instanceof ClientPresence) { this.gateway?.sendPresence(this.presence.create())
this.presence = presence }
} else this.presence = new ClientPresence(presence)
this.gateway?.sendPresence(this.presence.create()) /** Emits debug event */
} debug(tag: string, msg: string): void {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
/** Emits debug event */ this.emit('debug', `[${tag}] ${msg}`)
debug(tag: string, msg: string): void { }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.emit('debug', `[${tag}] ${msg}`) getEstimatedID(): string {
} if (this.user !== undefined) return this.user.id
else if (this.token !== undefined) {
getEstimatedID(): string { try {
if (this.user !== undefined) return this.user.id return atob(this.token.split('.')[0])
else if (this.token !== undefined) { } catch (e) {
try { return this._id ?? 'unknown'
return atob(this.token.split('.')[0]) }
} catch (e) { } else {
return this._id ?? 'unknown' return this._id ?? 'unknown'
} }
} else { }
return this._id ?? 'unknown'
} /** Fetch Application of the Client */
} async fetchApplication(): Promise<Application> {
const app = await this.rest.api.oauth2.applications['@me'].get()
/** Fetch Application of the Client */ return new Application(this, app)
async fetchApplication(): Promise<Application> { }
const app = await this.rest.api.oauth2.applications['@me'].get()
return new Application(this, app) /** Fetch an Invite */
} async fetchInvite(id: string): Promise<Invite> {
return await new Promise((resolve, reject) => {
/** Fetch an Invite */ this.rest
async fetchInvite(id: string): Promise<Invite> { .get(INVITE(id))
return await new Promise((resolve, reject) => { .then((data) => {
this.rest resolve(new Invite(this, data))
.get(INVITE(id)) })
.then((data) => { .catch((e) => reject(e))
resolve(new Invite(this, data)) })
}) }
.catch((e) => reject(e))
}) /**
} * This function is used for connecting to discord.
* @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.
* This function is used for connecting to discord. */
* @param token Your token. This is required if not given in ClientOptions. async connect(
* @param intents Gateway intents in array. This is required if not given in ClientOptions. token?: string,
*/ intents?: Array<GatewayIntents | keyof typeof GatewayIntents>
async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> { ): 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
if (intents !== undefined && this.intents !== undefined) { if (intents !== undefined && this.intents !== undefined) {
this.debug( this.debug(
'client', 'client',
'Intents were set in both client and connect function. Using the one in the connect function...' 'Intents were set in both client and connect function. Using the one in the connect function...'
) )
} 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) =>
} else throw new Error('No Gateway Intents were provided') typeof e === 'string' ? GatewayIntents[e] : e
)
this.rest.token = token } else throw new Error('No Gateway Intents were provided')
if (this.shard !== undefined) {
if (typeof this.shardCount === 'number') this.rest.token = token
this.shards.cachedShardCount = this.shardCount if (this.shard !== undefined) {
await this.shards.launch(this.shard) if (typeof this.shardCount === 'number')
} else await this.shards.connect() this.shards.cachedShardCount = this.shardCount
return this.waitFor('ready', () => true).then(() => this) await this.shards.launch(this.shard)
} } else await this.shards.connect()
return this.waitFor('ready', () => true).then(() => this)
/** Destroy the Gateway connection */ }
async destroy(): Promise<Client> {
this.gateway.initialized = false /** Destroy the Gateway connection */
this.gateway.sequenceID = undefined async destroy(): Promise<Client> {
this.gateway.sessionID = undefined this.gateway.initialized = false
await this.gateway.cache.delete('seq') this.gateway.sequenceID = undefined
await this.gateway.cache.delete('session_id') this.gateway.sessionID = undefined
this.gateway.close() await this.gateway.cache.delete('seq')
this.user = undefined await this.gateway.cache.delete('session_id')
this.upSince = undefined this.gateway.close()
return this this.user = undefined
} this.upSince = undefined
return this
/** Attempt to Close current Gateway connection and Resume */ }
async reconnect(): Promise<Client> {
this.gateway.close() /** Attempt to Close current Gateway connection and Resume */
this.gateway.initWebsocket() async reconnect(): Promise<Client> {
return this.waitFor('ready', () => true).then(() => this) this.gateway.close()
} this.gateway.initWebsocket()
return this.waitFor('ready', () => true).then(() => this)
/** Add a new Collector */ }
addCollector(collector: Collector): boolean {
if (this.collectors.has(collector)) return false /** Add a new Collector */
else { addCollector(collector: Collector): boolean {
this.collectors.add(collector) if (this.collectors.has(collector)) return false
return true else {
} this.collectors.add(collector)
} return true
}
/** Remove a Collector */ }
removeCollector(collector: Collector): boolean {
if (!this.collectors.has(collector)) return false /** Remove a Collector */
else { removeCollector(collector: Collector): boolean {
this.collectors.delete(collector) if (!this.collectors.has(collector)) return false
return true else {
} this.collectors.delete(collector)
} return true
}
async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> { }
const collectors: Collector[] = []
for (const collector of this.collectors.values()) { async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> {
if (collector.event === event) collectors.push(collector) const collectors: Collector[] = []
} for (const collector of this.collectors.values()) {
if (collectors.length !== 0) { if (collector.event === event) collectors.push(collector)
this.collectors.forEach((collector) => collector._fire(...args)) }
} if (collectors.length !== 0) {
// TODO(DjDeveloperr): Fix this ts-ignore this.collectors.forEach((collector) => collector._fire(...args))
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error }
// @ts-ignore // TODO(DjDeveloperr): Fix this ts-ignore
return super.emit(event, ...args) // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
} // @ts-ignore
return super.emit(event, ...args)
/** Returns an array of voice region objects that can be used when creating servers. */ }
async fetchVoiceRegions(): Promise<VoiceRegion[]> {
return this.rest.api.voice.regions.get() /** Returns an array of voice region objects that can be used when creating servers. */
} async fetchVoiceRegions(): Promise<VoiceRegion[]> {
return this.rest.api.voice.regions.get()
/** Modify current (Client) User. */ }
async editUser(data: {
username?: string /** Modify current (Client) User. */
avatar?: string async editUser(data: {
}): Promise<Client> { username?: string
if (data.username === undefined && data.avatar === undefined) avatar?: string
throw new Error( }): Promise<Client> {
'Either username or avatar or both must be specified to edit' if (data.username === undefined && data.avatar === undefined)
) throw new Error(
'Either username or avatar or both must be specified to edit'
if (data.avatar?.startsWith('http') === true) { )
data.avatar = await fetchAuto(data.avatar)
} if (data.avatar?.startsWith('http') === true) {
data.avatar = await fetchAuto(data.avatar)
await this.rest.api.users['@me'].patch({ }
username: data.username,
avatar: data.avatar await this.rest.api.users['@me'].patch({
}) username: data.username,
return this avatar: data.avatar
} })
return this
/** Change Username of the Client User */ }
async setUsername(username: string): Promise<Client> {
return await this.editUser({ username }) /** Change Username of the Client User */
} async setUsername(username: string): Promise<Client> {
return await this.editUser({ username })
/** Change Avatar of the Client User */ }
async setAvatar(avatar: string): Promise<Client> {
return await this.editUser({ avatar }) /** Change Avatar of the Client User */
} async setAvatar(avatar: string): Promise<Client> {
return await this.editUser({ avatar })
/** Create a DM Channel with a User */ }
async createDM(user: User | string): Promise<DMChannel> {
const id = typeof user === 'object' ? user.id : user /** Create a DM Channel with a User */
const dmPayload = await this.rest.api.users['@me'].channels.post({ async createDM(user: User | string): Promise<DMChannel> {
recipient_id: id const id = typeof user === 'object' ? user.id : user
}) const dmPayload = await this.rest.api.users['@me'].channels.post({
await this.channels.set(dmPayload.id, dmPayload) recipient_id: id
return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel })
} await this.channels.set(dmPayload.id, dmPayload)
return (this.channels.get<DMChannel>(dmPayload.id) as unknown) as DMChannel
/** Returns a template object for the given code. */ }
async fetchTemplate(code: string): Promise<Template> {
const payload = await this.rest.api.guilds.templates[code].get() /** Returns a template object for the given code. */
return new Template(this, payload) async fetchTemplate(code: string): Promise<Template> {
} const payload = await this.rest.api.guilds.templates[code].get()
} return new Template(this, payload)
}
/** Event decorator to create an Event handler from function */ }
export function event(name?: keyof ClientEvents) {
return function ( /** Event decorator to create an Event handler from function */
client: Client | Extension, export function event(name?: keyof ClientEvents) {
prop: keyof ClientEvents | string return function (
) { client: Client | Extension,
const listener = ((client as unknown) as { prop: keyof ClientEvents | string
[name in keyof ClientEvents]: (...args: ClientEvents[name]) => any ) {
})[(prop as unknown) as keyof ClientEvents] const listener = ((client as unknown) as {
if (typeof listener !== 'function') [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any
throw new Error('@event decorator requires a function') })[(prop as unknown) as keyof ClientEvents]
if (client._decoratedEvents === undefined) client._decoratedEvents = {} if (typeof listener !== 'function')
const key = name === undefined ? prop : name throw new Error('@event decorator requires a function')
if (client._decoratedEvents === undefined) client._decoratedEvents = {}
client._decoratedEvents[key] = listener const key = name === undefined ? prop : name
}
} 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
})
}
}

View File

@ -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
View File

@ -0,0 +1,3 @@
export * from './client.ts'
export * from './collectors.ts'
export * from './shard.ts'

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,3 @@
export * from './client.ts'
export * from './command.ts'
export * from './extension.ts'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]]
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,3 @@
export * from './slashClient.ts'
export * from './slashModule.ts'
export * from './slashCommand.ts'

View 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
})
}
}

View File

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

View File

@ -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 = ''

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

44
src/rest/error.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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']

View File

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

View File

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

View File

@ -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 = (

View File

@ -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'
@ -81,7 +81,7 @@ export class GuildChannel extends Channel {
const stringToObject = const stringToObject =
typeof target === 'string' typeof target === 'string'
? (await this.guild.members.get(target)) ?? ? (await this.guild.members.get(target)) ??
(await this.guild.roles.get(target)) (await this.guild.roles.get(target))
: target : target
if (stringToObject === undefined) { if (stringToObject === undefined) {
@ -128,7 +128,7 @@ export class GuildChannel extends Channel {
const stringToObject = const stringToObject =
typeof target === 'string' typeof target === 'string'
? (await this.guild.members.get(target)) ?? ? (await this.guild.members.get(target)) ??
(await this.guild.roles.get(target)) (await this.guild.roles.get(target))
: target : target
if (stringToObject === undefined) { if (stringToObject === undefined) {
@ -200,8 +200,8 @@ export class GuildChannel extends Channel {
overwrite.id instanceof Role overwrite.id instanceof Role
? 0 ? 0
: overwrite.id instanceof Member : overwrite.id instanceof Member
? 1 ? 1
: overwrite.type : overwrite.type
if (type === undefined) { if (type === undefined) {
throw new Error('Overwrite type is undefined.') throw new Error('Overwrite type is undefined.')
} }
@ -233,8 +233,8 @@ export class GuildChannel extends Channel {
overwrite.id instanceof Role overwrite.id instanceof Role
? 0 ? 0
: overwrite.id instanceof Member : overwrite.id instanceof Member
? 1 ? 1
: overwrite.type : overwrite.type
if (type === undefined) { if (type === undefined) {
throw new Error('Overwrite type is undefined.') throw new Error('Overwrite type is undefined.')
} }
@ -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)
@ -331,8 +334,8 @@ export class GuildChannel extends Channel {
overwrite.id instanceof Role overwrite.id instanceof Role
? 0 ? 0
: overwrite.id instanceof Member : overwrite.id instanceof Member
? 1 ? 1
: overwrite.type : overwrite.type
if (type === undefined) { if (type === undefined) {
throw new Error('Overwrite type is undefined.') throw new Error('Overwrite type is undefined.')
} }

View File

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

View File

@ -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,44 +56,72 @@ 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,
type: this.type, type: this.type,

Some files were not shown because too many files have changed in this diff Show More