rework rest impl
This commit is contained in:
parent
5e65673107
commit
b479cdc743
|
@ -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
|
||||||
|
|
|
@ -266,21 +266,6 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
|
||||||
if (typeof this.client.intents !== 'object')
|
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'}`
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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}${
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
sublimitTimeout ? ' 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,66 +1,10 @@
|
||||||
import { Embed } from '../structures/embed.ts'
|
|
||||||
import { MessageAttachment } from '../structures/message.ts'
|
|
||||||
import { Collection } from '../utils/collection.ts'
|
import { Collection } from '../utils/collection.ts'
|
||||||
import type { Client } from '../client/mod.ts'
|
import type { Client } from '../client/mod.ts'
|
||||||
import { simplifyAPIError } from '../utils/err_fmt.ts'
|
import { RequestMethods, METHODS } from './types.ts'
|
||||||
import {
|
|
||||||
DiscordAPIErrorPayload,
|
|
||||||
HttpResponseCode,
|
|
||||||
RequestHeaders,
|
|
||||||
RequestMethods,
|
|
||||||
METHODS
|
|
||||||
} from './types.ts'
|
|
||||||
import { Constants } from '../types/constants.ts'
|
import { Constants } from '../types/constants.ts'
|
||||||
import { RESTEndpoints } from './endpoints.ts'
|
import { RESTEndpoints } from './endpoints.ts'
|
||||||
|
import { BucketHandler } from './bucket.ts'
|
||||||
export class DiscordAPIError extends Error {
|
import { APIRequest, RequestOptions } from './request.ts'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MethodFunction = (
|
export type MethodFunction = (
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
|
@ -126,6 +70,10 @@ export interface RESTOptions {
|
||||||
userAgent?: string
|
userAgent?: string
|
||||||
/** Optional Harmony client */
|
/** Optional Harmony client */
|
||||||
client?: Client
|
client?: Client
|
||||||
|
/** Requests Timeout (in MS, default 30s) */
|
||||||
|
requestTimeout?: number
|
||||||
|
/** Retry Limit (default 1) */
|
||||||
|
retryLimit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Token Type for REST API. */
|
/** Token Type for REST API. */
|
||||||
|
@ -140,12 +88,6 @@ export enum TokenType {
|
||||||
|
|
||||||
/** An easier to use interface for interacting with Discord REST API. */
|
/** An easier to use interface for interacting with Discord REST API. */
|
||||||
export class RESTManager {
|
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 */
|
/** API Version being used by REST Manager */
|
||||||
version: number = 8
|
version: number = 8
|
||||||
/**
|
/**
|
||||||
|
@ -173,6 +115,17 @@ export class RESTManager {
|
||||||
/** Optional Harmony Client object */
|
/** Optional Harmony Client object */
|
||||||
client?: Client
|
client?: Client
|
||||||
endpoints: RESTEndpoints
|
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) {
|
constructor(options?: RESTOptions) {
|
||||||
this.api = builder(this)
|
this.api = builder(this)
|
||||||
|
@ -183,294 +136,35 @@ export class RESTManager {
|
||||||
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
|
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
|
||||||
if (options?.canary !== undefined) this.canary = options.canary
|
if (options?.canary !== undefined) this.canary = options.canary
|
||||||
if (options?.client !== undefined) this.client = options.client
|
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)
|
this.endpoints = new RESTEndpoints(this)
|
||||||
this.handleRateLimits()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks the queues of buckets, if empty, delete entry */
|
setTimeout(fn: (...args: any[]) => any, ms: number): number {
|
||||||
private checkQueues(): void {
|
const timer = setTimeout(async () => {
|
||||||
Object.entries(this.queues).forEach(([key, value]) => {
|
this.timers.delete(timer)
|
||||||
if (value.length === 0) {
|
await fn()
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
}, ms)
|
||||||
delete this.queues[key]
|
this.timers.add(timer)
|
||||||
}
|
return timer
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Adds a Request to Queue */
|
async request<T = any>(
|
||||||
private queue(request: QueuedItem): void {
|
method: RequestMethods,
|
||||||
const route = request.url.substring(
|
path: string,
|
||||||
Number(Constants.DISCORD_API_URL.length) + 1
|
options: RequestOptions = {}
|
||||||
)
|
): Promise<T> {
|
||||||
const parts = route.split('/')
|
const req = new APIRequest(this, method, path, options)
|
||||||
parts.shift()
|
let handler = this.handlers.get(req.path)
|
||||||
const [id] = parts
|
|
||||||
|
|
||||||
if (this.queues[id] !== undefined) {
|
if (handler === undefined) {
|
||||||
this.queues[id].push(request)
|
handler = new BucketHandler(this)
|
||||||
} else {
|
this.handlers.set(req.route, handler)
|
||||||
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) {
|
return handler.push(req)
|
||||||
// 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'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -486,109 +180,23 @@ export class RESTManager {
|
||||||
method: RequestMethods,
|
method: RequestMethods,
|
||||||
url: string,
|
url: string,
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
maxRetries = 0,
|
_maxRetries = 0,
|
||||||
bucket?: string | null,
|
bucket?: string | null,
|
||||||
rawResponse?: boolean
|
rawResponse?: boolean,
|
||||||
|
options: RequestOptions = {}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await this.request(
|
||||||
const onComplete = async (): Promise<undefined | any> => {
|
method,
|
||||||
try {
|
url,
|
||||||
const rateLimitResetIn = await this.isRateLimited(url)
|
Object.assign(
|
||||||
if (rateLimitResetIn !== false) {
|
{
|
||||||
return {
|
data: body,
|
||||||
rateLimited: rateLimitResetIn,
|
rawResponse,
|
||||||
before: true,
|
route: bucket ?? undefined
|
||||||
bucket
|
},
|
||||||
}
|
options
|
||||||
}
|
)
|
||||||
|
)
|
||||||
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
|
|
||||||
Constants.DISCORD_API_URL +
|
|
||||||
'/v' +
|
|
||||||
Constants.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 */
|
/** Makes a GET Request to API */
|
||||||
|
@ -597,9 +205,18 @@ export class RESTManager {
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
maxRetries = 0,
|
maxRetries = 0,
|
||||||
bucket?: string | null,
|
bucket?: string | null,
|
||||||
rawResponse?: boolean
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.make('get', url, body, maxRetries, bucket, rawResponse)
|
return await this.make(
|
||||||
|
'get',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Makes a POST Request to API */
|
/** Makes a POST Request to API */
|
||||||
|
@ -608,9 +225,18 @@ export class RESTManager {
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
maxRetries = 0,
|
maxRetries = 0,
|
||||||
bucket?: string | null,
|
bucket?: string | null,
|
||||||
rawResponse?: boolean
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.make('post', url, body, maxRetries, bucket, rawResponse)
|
return await this.make(
|
||||||
|
'post',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Makes a DELETE Request to API */
|
/** Makes a DELETE Request to API */
|
||||||
|
@ -619,9 +245,18 @@ export class RESTManager {
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
maxRetries = 0,
|
maxRetries = 0,
|
||||||
bucket?: string | null,
|
bucket?: string | null,
|
||||||
rawResponse?: boolean
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.make('delete', url, body, maxRetries, bucket, rawResponse)
|
return await this.make(
|
||||||
|
'delete',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Makes a PATCH Request to API */
|
/** Makes a PATCH Request to API */
|
||||||
|
@ -630,9 +265,18 @@ export class RESTManager {
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
maxRetries = 0,
|
maxRetries = 0,
|
||||||
bucket?: string | null,
|
bucket?: string | null,
|
||||||
rawResponse?: boolean
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.make('patch', url, body, maxRetries, bucket, rawResponse)
|
return await this.make(
|
||||||
|
'patch',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Makes a PUT Request to API */
|
/** Makes a PUT Request to API */
|
||||||
|
@ -641,8 +285,17 @@ export class RESTManager {
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
maxRetries = 0,
|
maxRetries = 0,
|
||||||
bucket?: string | null,
|
bucket?: string | null,
|
||||||
rawResponse?: boolean
|
rawResponse?: boolean,
|
||||||
|
options?: RequestOptions
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return await this.make('put', url, body, maxRetries, bucket, rawResponse)
|
return await this.make(
|
||||||
|
'put',
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
maxRetries,
|
||||||
|
bucket,
|
||||||
|
rawResponse,
|
||||||
|
options
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
export * from './manager.ts'
|
export * from './manager.ts'
|
||||||
export * from './types.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'
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,9 @@ export function simplifyAPIError(errors: any): SimplifiedError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.entries(errors).forEach((obj: [string, any]) => {
|
Object.entries(errors).forEach((obj: [string, any]) => {
|
||||||
fmt(obj[1], obj[0])
|
if (obj[0] === '_errors') {
|
||||||
|
fmt({ _errors: obj[1] }, 'Request')
|
||||||
|
} else fmt(obj[1], obj[0])
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue