mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 12:03:31 +01:00
feat(builders): multipart form data output support (#11248)
* feat(builders): multipart form data output support * refactor: proper key management * chore: add missing remarks * chore: requested changes * chore: rename encodables file * chore: requested changes * chore: requested changes * chore: nits Co-authored-by: Almeida <github@almeidx.dev> * chore: requested change * chore: requested change --------- Co-authored-by: Almeida <github@almeidx.dev>
This commit is contained in:
committed by
GitHub
parent
315f422781
commit
68bb8af58a
55
packages/builders/__tests__/messages/fileBody.test.ts
Normal file
55
packages/builders/__tests__/messages/fileBody.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
import type { RawFile } from '@discordjs/util';
|
||||||
|
import { test, expect } from 'vitest';
|
||||||
|
import { AttachmentBuilder, MessageBuilder } from '../../src/index.js';
|
||||||
|
|
||||||
|
test('AttachmentBuilder stores and exposes file data', () => {
|
||||||
|
const data = Buffer.from('hello world');
|
||||||
|
const attachment = new AttachmentBuilder()
|
||||||
|
.setId('0')
|
||||||
|
.setFilename('greeting.txt')
|
||||||
|
.setFileData(data)
|
||||||
|
.setFileContentType('text/plain');
|
||||||
|
|
||||||
|
expect(attachment.getRawFile()).toStrictEqual({
|
||||||
|
contentType: 'text/plain',
|
||||||
|
data,
|
||||||
|
key: 'files[0]',
|
||||||
|
name: 'greeting.txt',
|
||||||
|
});
|
||||||
|
|
||||||
|
attachment.clearFileData();
|
||||||
|
attachment.clearFileContentType();
|
||||||
|
attachment.clearFilename();
|
||||||
|
expect(attachment.getRawFile()).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MessageBuilder.toFileBody returns JSON body and files', () => {
|
||||||
|
const msg = new MessageBuilder().setContent('here is a file').addAttachments(
|
||||||
|
new AttachmentBuilder()
|
||||||
|
.setId('0')
|
||||||
|
.setFilename('file.bin')
|
||||||
|
.setFileData(Buffer.from([1, 2, 3]))
|
||||||
|
.setFileContentType('application/octet-stream'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { body, files } = msg.toFileBody();
|
||||||
|
|
||||||
|
// body should match toJSON()
|
||||||
|
expect(body).toStrictEqual(msg.toJSON());
|
||||||
|
|
||||||
|
// files should contain the uploaded file
|
||||||
|
expect(files).toHaveLength(1);
|
||||||
|
const [fileEntry] = files as [RawFile];
|
||||||
|
expect(fileEntry.name).toBe('file.bin');
|
||||||
|
expect(fileEntry.contentType).toBe('application/octet-stream');
|
||||||
|
expect(fileEntry.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MessageBuilder.toFileBody returns empty files when attachments reference existing uploads', () => {
|
||||||
|
const msg = new MessageBuilder().addAttachments(new AttachmentBuilder().setId('123').setFilename('existing.png'));
|
||||||
|
|
||||||
|
const { body, files } = msg.toFileBody();
|
||||||
|
expect(body).toEqual(msg.toJSON());
|
||||||
|
expect(files.length).toBe(0);
|
||||||
|
});
|
||||||
@@ -97,6 +97,9 @@ export * from './util/ValidationError.js';
|
|||||||
|
|
||||||
export * from './Assertions.js';
|
export * from './Assertions.js';
|
||||||
|
|
||||||
|
// We expose this type in our public API. We shouldn't assume every user of builders is also using REST
|
||||||
|
export type { RawFile } from '@discordjs/util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version
|
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version
|
||||||
* that you are currently using.
|
* that you are currently using.
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
|
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { embedPredicate } from './embed/Assertions.js';
|
import { embedPredicate } from './embed/Assertions.js';
|
||||||
import { pollPredicate } from './poll/Assertions.js';
|
import { pollPredicate } from './poll/Assertions.js';
|
||||||
|
|
||||||
|
const fileKeyRegex = /^files\[(?<placeholder>\d+?)]$/;
|
||||||
|
|
||||||
|
export const rawFilePredicate = z.object({
|
||||||
|
data: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array), z.string()]),
|
||||||
|
name: z.string().min(1),
|
||||||
|
contentType: z.string().optional(),
|
||||||
|
key: z.string().regex(fileKeyRegex).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const attachmentPredicate = z.object({
|
export const attachmentPredicate = z.object({
|
||||||
|
// As a string it only makes sense for edits when we do have an attachment snowflake
|
||||||
id: z.union([z.string(), z.number()]),
|
id: z.union([z.string(), z.number()]),
|
||||||
description: z.string().max(1_024).optional(),
|
description: z.string().max(1_024).optional(),
|
||||||
duration_secs: z
|
duration_secs: z
|
||||||
@@ -125,3 +136,11 @@ const messageComponentsV2Predicate = baseMessagePredicate.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const messagePredicate = z.union([messageNoComponentsV2Predicate, messageComponentsV2Predicate]);
|
export const messagePredicate = z.union([messageNoComponentsV2Predicate, messageComponentsV2Predicate]);
|
||||||
|
|
||||||
|
// This validator does not assert file.key <-> attachment.id coherence. This is fine, because the builders
|
||||||
|
// should effectively guarantee that.
|
||||||
|
export const fileBodyMessagePredicate = z.object({
|
||||||
|
body: messagePredicate,
|
||||||
|
// No min length to support message edits
|
||||||
|
files: rawFilePredicate.array().max(10),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { JSONEncodable } from '@discordjs/util';
|
import type { Buffer } from 'node:buffer';
|
||||||
|
import type { JSONEncodable, RawFile } from '@discordjs/util';
|
||||||
import type { RESTAPIAttachment, Snowflake } from 'discord-api-types/v10';
|
import type { RESTAPIAttachment, Snowflake } from 'discord-api-types/v10';
|
||||||
import { validate } from '../util/validation.js';
|
import { validate } from '../util/validation.js';
|
||||||
import { attachmentPredicate } from './Assertions.js';
|
import { attachmentPredicate } from './Assertions.js';
|
||||||
@@ -12,6 +13,17 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
|
|||||||
*/
|
*/
|
||||||
private readonly data: Partial<RESTAPIAttachment>;
|
private readonly data: Partial<RESTAPIAttachment>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This data is not included in the output of `toJSON()`. For this class specifically, this refers to binary data
|
||||||
|
* that will wind up being included in the multipart/form-data request, if used with the `MessageBuilder`.
|
||||||
|
* To retrieve this data, use {@link getRawFile}.
|
||||||
|
*
|
||||||
|
* @remarks This cannot be set via the constructor, primarily because of the behavior described
|
||||||
|
* {@link https://discord.com/developers/docs/reference#editing-message-attachments | here}.
|
||||||
|
* That is, when editing a message's attachments, you should only be providing file data for new attachments.
|
||||||
|
*/
|
||||||
|
private readonly fileData: Partial<Pick<RawFile, 'contentType' | 'data'>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new attachment builder.
|
* Creates a new attachment builder.
|
||||||
*
|
*
|
||||||
@@ -19,6 +31,7 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
|
|||||||
*/
|
*/
|
||||||
public constructor(data: Partial<RESTAPIAttachment> = {}) {
|
public constructor(data: Partial<RESTAPIAttachment> = {}) {
|
||||||
this.data = structuredClone(data);
|
this.data = structuredClone(data);
|
||||||
|
this.fileData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +39,7 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
|
|||||||
*
|
*
|
||||||
* @param id - The id of the attachment
|
* @param id - The id of the attachment
|
||||||
*/
|
*/
|
||||||
public setId(id: Snowflake): this {
|
public setId(id: Snowflake | number): this {
|
||||||
this.data.id = id;
|
this.data.id = id;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -85,6 +98,60 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the file data to upload with this attachment.
|
||||||
|
*
|
||||||
|
* @param data - The file data
|
||||||
|
* @remarks Note that this data is NOT included in the {@link toJSON} output. To retrieve it, use {@link getRawFile}.
|
||||||
|
*/
|
||||||
|
public setFileData(data: Buffer | Uint8Array | string): this {
|
||||||
|
this.fileData.data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the file data from this attachment.
|
||||||
|
*/
|
||||||
|
public clearFileData(): this {
|
||||||
|
this.fileData.data = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the content type of the file data to upload with this attachment.
|
||||||
|
*
|
||||||
|
* @remarks Note that this data is NOT included in the {@link toJSON} output. To retrieve it, use {@link getRawFile}.
|
||||||
|
*/
|
||||||
|
public setFileContentType(contentType: string): this {
|
||||||
|
this.fileData.contentType = contentType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the content type of the file data from this attachment.
|
||||||
|
*/
|
||||||
|
public clearFileContentType(): this {
|
||||||
|
this.fileData.contentType = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this attachment to a {@link RawFile} for uploading.
|
||||||
|
*
|
||||||
|
* @returns A {@link RawFile} object, or `undefined` if no file data is set
|
||||||
|
*/
|
||||||
|
public getRawFile(): Partial<RawFile> | undefined {
|
||||||
|
if (!this.fileData?.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this.fileData,
|
||||||
|
name: this.data.filename,
|
||||||
|
key: this.data.id ? `files[${this.data.id}]` : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the title of this attachment.
|
* Sets the title of this attachment.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { JSONEncodable } from '@discordjs/util';
|
import type { FileBodyEncodable, FileBodyEncodableResult, JSONEncodable, RawFile } from '@discordjs/util';
|
||||||
import type {
|
import type {
|
||||||
APIActionRowComponent,
|
APIActionRowComponent,
|
||||||
APIAllowedMentions,
|
APIAllowedMentions,
|
||||||
@@ -32,7 +32,7 @@ import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
|||||||
import { resolveBuilder } from '../util/resolveBuilder.js';
|
import { resolveBuilder } from '../util/resolveBuilder.js';
|
||||||
import { validate } from '../util/validation.js';
|
import { validate } from '../util/validation.js';
|
||||||
import { AllowedMentionsBuilder } from './AllowedMentions.js';
|
import { AllowedMentionsBuilder } from './AllowedMentions.js';
|
||||||
import { messagePredicate } from './Assertions.js';
|
import { fileBodyMessagePredicate, messagePredicate } from './Assertions.js';
|
||||||
import { AttachmentBuilder } from './Attachment.js';
|
import { AttachmentBuilder } from './Attachment.js';
|
||||||
import { MessageReferenceBuilder } from './MessageReference.js';
|
import { MessageReferenceBuilder } from './MessageReference.js';
|
||||||
import { EmbedBuilder } from './embed/Embed.js';
|
import { EmbedBuilder } from './embed/Embed.js';
|
||||||
@@ -56,7 +56,9 @@ export interface MessageBuilderData
|
|||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for messages.
|
* A builder that creates API-compatible JSON data for messages.
|
||||||
*/
|
*/
|
||||||
export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJSONBody> {
|
export class MessageBuilder
|
||||||
|
implements JSONEncodable<RESTPostAPIChannelMessageJSONBody>, FileBodyEncodable<RESTPostAPIChannelMessageJSONBody>
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* The API data associated with this message.
|
* The API data associated with this message.
|
||||||
*/
|
*/
|
||||||
@@ -661,4 +663,31 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
|||||||
|
|
||||||
return data as RESTPostAPIChannelMessageJSONBody;
|
return data as RESTPostAPIChannelMessageJSONBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to both JSON body and file data for multipart/form-data requests.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
* @remarks
|
||||||
|
* This method extracts file data from attachments that have files set via {@link AttachmentBuilder.setFileData}.
|
||||||
|
* The returned body includes attachment metadata, while files contains the binary data for upload.
|
||||||
|
*/
|
||||||
|
public toFileBody(validationOverride?: boolean): FileBodyEncodableResult<RESTPostAPIChannelMessageJSONBody> {
|
||||||
|
const body = this.toJSON(false);
|
||||||
|
|
||||||
|
const files: RawFile[] = [];
|
||||||
|
for (const attachment of this.data.attachments) {
|
||||||
|
const rawFile = attachment.getRawFile();
|
||||||
|
// Only if data or content type are set, since that implies the intent is to send a new file.
|
||||||
|
// In case it's contentType but not data, a validation error will be thrown right after.
|
||||||
|
if (rawFile?.data || rawFile?.contentType) {
|
||||||
|
files.push(rawFile as RawFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = { body, files };
|
||||||
|
validate(fileBodyMessagePredicate, combined, validationOverride);
|
||||||
|
|
||||||
|
return combined as FileBodyEncodableResult<RESTPostAPIChannelMessageJSONBody>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Readable } from 'node:stream';
|
import type { Readable } from 'node:stream';
|
||||||
import type { ReadableStream } from 'node:stream/web';
|
import type { ReadableStream } from 'node:stream/web';
|
||||||
import type { Collection } from '@discordjs/collection';
|
import type { Collection } from '@discordjs/collection';
|
||||||
import type { Awaitable } from '@discordjs/util';
|
import type { Awaitable, RawFile } from '@discordjs/util';
|
||||||
import type { Agent, Dispatcher, RequestInit, BodyInit, Response } from 'undici';
|
import type { Agent, Dispatcher, RequestInit, BodyInit, Response } from 'undici';
|
||||||
import type { IHandler } from '../interfaces/Handler.js';
|
import type { IHandler } from '../interfaces/Handler.js';
|
||||||
|
|
||||||
@@ -276,29 +276,7 @@ export interface InvalidRequestWarningData {
|
|||||||
remainingTime: number;
|
remainingTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export type { RawFile } from '@discordjs/util';
|
||||||
* Represents a file to be added to the request
|
|
||||||
*/
|
|
||||||
export interface RawFile {
|
|
||||||
/**
|
|
||||||
* Content-Type of the file
|
|
||||||
*/
|
|
||||||
contentType?: string;
|
|
||||||
/**
|
|
||||||
* The actual data for the file
|
|
||||||
*/
|
|
||||||
data: Buffer | Uint8Array | boolean | number | string;
|
|
||||||
/**
|
|
||||||
* An explicit key to use for key of the formdata field for this file.
|
|
||||||
* When not provided, the index of the file in the files array is used in the form `files[${index}]`.
|
|
||||||
* If you wish to alter the placeholder snowflake, you must provide this property in the same form (`files[${placeholder}]`)
|
|
||||||
*/
|
|
||||||
key?: string;
|
|
||||||
/**
|
|
||||||
* The name of the file
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthData {
|
export interface AuthData {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Represents an object capable of representing itself as a JSON object
|
|
||||||
*
|
|
||||||
* @typeParam Value - The JSON type corresponding to {@link JSONEncodable.toJSON} outputs.
|
|
||||||
*/
|
|
||||||
export interface JSONEncodable<Value> {
|
|
||||||
/**
|
|
||||||
* Transforms this object to its JSON format
|
|
||||||
*/
|
|
||||||
toJSON(): Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if an object is encodable or not.
|
|
||||||
*
|
|
||||||
* @param maybeEncodable - The object to check against
|
|
||||||
*/
|
|
||||||
export function isJSONEncodable(maybeEncodable: unknown): maybeEncodable is JSONEncodable<unknown> {
|
|
||||||
return maybeEncodable !== null && typeof maybeEncodable === 'object' && 'toJSON' in maybeEncodable;
|
|
||||||
}
|
|
||||||
34
packages/util/src/RawFile.ts
Normal file
34
packages/util/src/RawFile.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a file to be added to a request with multipart/form-data encoding
|
||||||
|
*/
|
||||||
|
export interface RawFile {
|
||||||
|
/**
|
||||||
|
* Content-Type of the file.
|
||||||
|
* If not provided, it will be inferred from the file data when possible
|
||||||
|
*
|
||||||
|
* @example 'image/png'
|
||||||
|
* @example 'application/pdf'
|
||||||
|
*/
|
||||||
|
contentType?: string;
|
||||||
|
/**
|
||||||
|
* The actual data for the file
|
||||||
|
*/
|
||||||
|
data: Buffer | Uint8Array | boolean | number | string;
|
||||||
|
/**
|
||||||
|
* An explicit key to use for the formdata field for this file.
|
||||||
|
* When not provided, the index of the file in the files array is used in the form `files[${index}]`.
|
||||||
|
* If you wish to alter the placeholder snowflake, you must provide this property in the same form (`files[${placeholder}]`)
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
/**
|
||||||
|
* The name of the file. This is the actual filename that will be used when uploading to Discord.
|
||||||
|
* This is also the name you'll use to reference the file with attachment:// URLs.
|
||||||
|
*
|
||||||
|
* @example 'image.png'
|
||||||
|
* @example 'document.pdf'
|
||||||
|
* @example 'SPOILER_secret.jpeg'
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
61
packages/util/src/encodables.ts
Normal file
61
packages/util/src/encodables.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { RawFile } from './RawFile.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an object capable of representing itself as a JSON object
|
||||||
|
*
|
||||||
|
* @typeParam Value - The JSON type corresponding to {@link JSONEncodable.toJSON} outputs.
|
||||||
|
*/
|
||||||
|
export interface JSONEncodable<Value> {
|
||||||
|
/**
|
||||||
|
* Transforms this object to its JSON format
|
||||||
|
*/
|
||||||
|
toJSON(): Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if an object is encodable or not.
|
||||||
|
*
|
||||||
|
* @param maybeEncodable - The object to check against
|
||||||
|
*/
|
||||||
|
export function isJSONEncodable(maybeEncodable: unknown): maybeEncodable is JSONEncodable<unknown> {
|
||||||
|
return maybeEncodable !== null && typeof maybeEncodable === 'object' && 'toJSON' in maybeEncodable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of encoding an object that includes file attachments
|
||||||
|
*
|
||||||
|
* @typeParam BodyValue - The JSON body type
|
||||||
|
*/
|
||||||
|
export interface FileBodyEncodableResult<BodyValue> {
|
||||||
|
/**
|
||||||
|
* The JSON body to send with the request
|
||||||
|
*/
|
||||||
|
body: BodyValue;
|
||||||
|
/**
|
||||||
|
* The files to attach to the request
|
||||||
|
*/
|
||||||
|
files: RawFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an object capable of representing itself as a request body with file attachments.
|
||||||
|
* Objects implementing this interface can separate JSON body data from binary file data,
|
||||||
|
* which is necessary for multipart/form-data requests.
|
||||||
|
*
|
||||||
|
* @typeParam BodyValue - The JSON body type
|
||||||
|
*/
|
||||||
|
export interface FileBodyEncodable<BodyValue> {
|
||||||
|
/**
|
||||||
|
* Transforms this object to its file body format, separating the JSON body from file attachments.
|
||||||
|
*/
|
||||||
|
toFileBody(): FileBodyEncodableResult<BodyValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if an object is file body encodable or not.
|
||||||
|
*
|
||||||
|
* @param maybeEncodable - The object to check against
|
||||||
|
*/
|
||||||
|
export function isFileBodyEncodable(maybeEncodable: unknown): maybeEncodable is FileBodyEncodable<unknown> {
|
||||||
|
return maybeEncodable !== null && typeof maybeEncodable === 'object' && 'toFileBody' in maybeEncodable;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export type * from './types.js';
|
export type * from './types.js';
|
||||||
export * from './functions/index.js';
|
export * from './functions/index.js';
|
||||||
export * from './JSONEncodable.js';
|
export * from './encodables.js';
|
||||||
|
export type * from './RawFile.js';
|
||||||
export * from './Equatable.js';
|
export * from './Equatable.js';
|
||||||
export * from './gatewayRateLimitError.js';
|
export * from './gatewayRateLimitError.js';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user