Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: node-fetch & @discordjs/formdata -> undici #6586

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to

## Installation

**Node.js 16.6.0 or newer is required.**
**Node.js 16.7.0 or newer is required.**

```sh-session
npm install discord.js
Expand Down
6 changes: 2 additions & 4 deletions packages/discord.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,9 @@
"@discordjs/builders": "^0.11.0",
"@discordjs/collection": "^0.4.0",
"@sapphire/async-queue": "^1.1.9",
"@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.26.0",
"form-data": "^4.0.0",
"node-fetch": "^2.6.1",
"undici": "^4.12.1",
"ws": "^8.4.0"
},
"devDependencies": {
Expand All @@ -77,6 +75,6 @@
"typescript": "^4.5.4"
},
"engines": {
"node": ">=16.6.0"
"node": ">=16.7.0"
}
}
8 changes: 6 additions & 2 deletions packages/discord.js/src/managers/GuildMemberManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const { Buffer } = require('node:buffer');
const { setTimeout } = require('node:timers');
const { types } = require('node:util');
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const { Error, TypeError, RangeError } = require('../errors');
Expand Down Expand Up @@ -115,7 +115,11 @@ class GuildMemberManager extends CachedManager {
}
const data = await this.client.api.guilds(this.guild.id).members(userId).put({ data: resolvedOptions });
// Data is an empty buffer if the member is already part of the guild.
return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data);
return types.isArrayBuffer(data)
? options.fetchWhenExisting === false
? null
: this.fetch(userId)
: this._add(data);
}

/**
Expand Down
4 changes: 1 addition & 3 deletions packages/discord.js/src/rest/APIRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

const https = require('node:https');
const { setTimeout } = require('node:timers');
const FormData = require('form-data');
const fetch = require('node-fetch');
const { fetch, FormData } = require('undici');
const { UserAgent } = require('../util/Constants');

let agent = null;
Expand Down Expand Up @@ -61,7 +60,6 @@ class APIRequest {
body.append('payload_json', JSON.stringify(this.options.data));
}
}
headers = Object.assign(headers, body.getHeaders());
// eslint-disable-next-line eqeqeq
} else if (this.options.data != null) {
body = JSON.stringify(this.options.data);
Expand Down
14 changes: 13 additions & 1 deletion packages/discord.js/src/rest/RequestHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ const {

function parseResponse(res) {
if (res.headers.get('content-type').startsWith('application/json')) return res.json();
return res.buffer();
return res.arrayBuffer();
}

async function consumeBody(res) {
if (res.body === null) return;
// eslint-disable-next-line no-unused-vars, no-empty
for await (const _chunk of res.body) {
}
}

function getAPIOffset(serverDate) {
Expand Down Expand Up @@ -306,6 +313,8 @@ class RequestHandler {
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
consumeBody(res);

const isGlobal = this.globalLimited;
let limit, timeout;
if (isGlobal) {
Expand Down Expand Up @@ -352,6 +361,8 @@ class RequestHandler {

// Handle 5xx responses
if (res.status >= 500 && res.status < 600) {
consumeBody(res);

// Retry the specified number of times for possible serverside issues
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(res.statusText, res.constructor.name, res.status, request);
Expand All @@ -361,6 +372,7 @@ class RequestHandler {
return this.execute(request);
}

consumeBody(res);
// Fallback in the rare case a status code outside the range 200..=599 is returned
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/discord.js/src/structures/MessageAttachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const Util = require('../util/Util');
*/
class MessageAttachment {
/**
* @param {BufferResolvable|Stream} attachment The file
* @param {BufferResolvable|Stream|Blob|File} attachment The file
* @param {string} [name=null] The name of the file, if any
* @param {APIAttachment} [data] Extra data
*/
Expand All @@ -17,7 +17,7 @@ class MessageAttachment {
* The name of this attachment
* @type {?string}
*/
this.name = name;
this.name = attachment.name ?? name;
if (data) this._patch(data);
}

Expand All @@ -33,7 +33,7 @@ class MessageAttachment {

/**
* Sets the file of this attachment.
* @param {BufferResolvable|Stream} attachment The file
* @param {BufferResolvable|Stream|Blob} attachment The file
* @param {string} [name=null] The name of the file, if any
* @returns {MessageAttachment} This attachment
*/
Expand Down
14 changes: 11 additions & 3 deletions packages/discord.js/src/structures/MessagePayload.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const { Buffer } = require('node:buffer');
const { Buffer, Blob } = require('node:buffer');
const BaseMessageComponent = require('./BaseMessageComponent');
const MessageEmbed = require('./MessageEmbed');
const { RangeError } = require('../errors');
Expand Down Expand Up @@ -219,7 +219,8 @@ class MessagePayload {

/**
* Resolves a single file into an object sendable to the API.
* @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file
* @param {BufferResolvable|Stream|FileOptions|MessageAttachment|Blob|File} fileLike Something that could be resolved
* to a file
* @returns {Promise<MessageFile>}
*/
static async resolveFile(fileLike) {
Expand All @@ -235,11 +236,18 @@ class MessagePayload {
return Util.basename(thing.path);
}

if (thing.name) {
return thing.name;
}

return 'file.jpg';
};

const ownAttachment =
typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function';
typeof fileLike === 'string' ||
fileLike instanceof Buffer ||
typeof fileLike.pipe === 'function' ||
fileLike instanceof Blob;
if (ownAttachment) {
attachment = fileLike;
name = findName(attachment);
Expand Down
45 changes: 29 additions & 16 deletions packages/discord.js/src/util/DataResolver.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict';

const { Buffer } = require('node:buffer');
const { Buffer, Blob } = require('node:buffer');
const fs = require('node:fs');
const path = require('node:path');
const stream = require('node:stream');
const fetch = require('node-fetch');
const { blob } = require('node:stream/consumers');
const { fetch } = require('undici');
const { Error: DiscordError, TypeError } = require('../errors');
const Invite = require('../structures/Invite');

Expand Down Expand Up @@ -57,16 +57,16 @@ class DataResolver extends null {
}

/**
* Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image.
* @param {BufferResolvable|Base64Resolvable} image The image to be resolved
* Resolves a Base64Resolvable, a string, Blob, or a BufferResolvable to a Base 64 image.
* @param {BufferResolvable|Base64Resolvable|Blob|File} image The image to be resolved
* @returns {Promise<?string>}
*/
static async resolveImage(image) {
if (!image) return null;
if (typeof image === 'string' && image.startsWith('data:')) {
return image;
}
const file = await this.resolveFileAsBuffer(image);
const file = await this.resolveFile(image);
return DataResolver.resolveBase64(file);
}

Expand All @@ -78,12 +78,18 @@ class DataResolver extends null {
*/

/**
* Resolves a Base64Resolvable to a Base 64 image.
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
* @returns {?string}
* Resolves a Base64Resolvable or Blob to a Base 64 image.
* @param {Base64Resolvable|Blob|File} data The base 64 resolvable you want to resolve
* @returns {Promise<string>}
*/
static resolveBase64(data) {
static async resolveBase64(data) {
if (Buffer.isBuffer(data)) return `data:image/jpg;base64,${data.toString('base64')}`;
// File is an instance of Blob
if (data instanceof Blob) {
const text = await data.text();
const buffer = Buffer.from(text);
return `data:${data.type || 'image/jpg'};base64,${buffer.toString('base64')}`;
}
return data;
}

Expand All @@ -102,24 +108,31 @@ class DataResolver extends null {
*/

/**
* Resolves a BufferResolvable to a Buffer or a Stream.
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
* @returns {Promise<Buffer|Stream>}
* Resolves a BufferResolvable, Blob, or Stream to a Blob.
* @param {BufferResolvable|Stream|Blob|File} resource The buffer or stream resolvable to resolve
* @returns {Promise<Blob>}
*/
static async resolveFile(resource) {
if (Buffer.isBuffer(resource) || resource instanceof stream.Readable) return resource;
if (resource instanceof Blob) return resource;
if (Buffer.isBuffer(resource)) return new Blob([resource]);
if (resource?.[Symbol.asyncIterator]) {
return blob(resource);
}
if (typeof resource === 'string') {
if (/^https?:\/\//.test(resource)) {
const res = await fetch(resource);
return res.body;
return res.blob();
}

return new Promise((resolve, reject) => {
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
if (err) return reject(err);
if (!stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file));
return resolve(fs.createReadStream(file));
return fs.readFile(file, (readErr, buff) => {
if (readErr) return reject(readErr);
return resolve(new Blob([buff]));
});
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/discord.js/src/util/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const { parse } = require('node:path');
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const fetch = require('node-fetch');
const { fetch } = require('undici');
const { Colors, Endpoints } = require('./Constants');
const Options = require('./Options');
const { Error: DiscordError, RangeError, TypeError } = require('../errors');
Expand Down
7 changes: 5 additions & 2 deletions packages/discord.js/test/sendtest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ const path = require('node:path');
const process = require('node:process');
const { setTimeout: sleep } = require('node:timers/promises');
const util = require('node:util');
const fetch = require('node-fetch');
const { fetch } = require('undici');
const { owner, token } = require('./auth.js');
const { Client, Intents, MessageAttachment, MessageEmbed } = require('../src');

const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] });

const buffer = l => fetch(l).then(res => res.buffer());
const buffer = l =>
fetch(l)
.then(res => res.arrayBuffer())
.then(buff => Buffer.from(buff));
const read = util.promisify(fs.readFile);
const readStream = fs.createReadStream;

Expand Down
7 changes: 5 additions & 2 deletions packages/discord.js/test/webhooktest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ const fs = require('node:fs');
const path = require('node:path');
const { setTimeout: sleep } = require('node:timers/promises');
const util = require('node:util');
const fetch = require('node-fetch');
const { fetch } = require('undici');
const { owner, token, webhookChannel, webhookToken } = require('./auth.js');
const { Client, Intents, MessageAttachment, MessageEmbed, WebhookClient } = require('../src');

const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] });

const buffer = l => fetch(l).then(res => res.buffer());
const buffer = l =>
fetch(l)
.then(res => res.arrayBuffer())
.then(buff => Buffer.from(buff));
const read = util.promisify(fs.readFile);
const readStream = fs.createReadStream;

Expand Down
15 changes: 8 additions & 7 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ import {
RESTPostAPIApplicationCommandsJSONBody,
Snowflake,
} from 'discord-api-types/v9';
import { Blob } from 'node:buffer';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { AgentOptions } from 'node:https';
import { Response } from 'node-fetch';
import { Stream } from 'node:stream';
import { File, Response } from 'undici';
import { MessagePort, Worker } from 'node:worker_threads';
import * as WebSocket from 'ws';
import {
Expand Down Expand Up @@ -824,11 +825,11 @@ export class ContextMenuInteraction<Cached extends CacheType = CacheType> extend

export class DataResolver extends null {
private constructor();
public static resolveBase64(data: Base64Resolvable): string;
public static resolveBase64(data: Base64Resolvable | Blob | File): Promise<string>;
public static resolveCode(data: string, regx: RegExp): string;
public static resolveFile(resource: BufferResolvable | Stream): Promise<Buffer | Stream>;
public static resolveFile(resource: BufferResolvable | Stream | Blob | File): Promise<Blob>;
public static resolveFileAsBuffer(resource: BufferResolvable | Stream): Promise<Buffer>;
public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise<string | null>;
public static resolveImage(resource: BufferResolvable | Base64Resolvable | Blob | File): Promise<string | null>;
public static resolveInviteCode(data: InviteResolvable): string;
public static resolveGuildTemplateCode(data: GuildTemplateResolvable): string;
}
Expand Down Expand Up @@ -1570,9 +1571,9 @@ export class MessageActionRow extends BaseMessageComponent {
}

export class MessageAttachment {
public constructor(attachment: BufferResolvable | Stream, name?: string, data?: RawMessageAttachmentData);
public constructor(attachment: BufferResolvable | Stream | Blob, name?: string, data?: RawMessageAttachmentData);

public attachment: BufferResolvable | Stream;
public attachment: BufferResolvable | Stream | Blob;
public contentType: string | null;
public description: string | null;
public ephemeral: boolean;
Expand All @@ -1585,7 +1586,7 @@ export class MessageAttachment {
public url: string;
public width: number | null;
public setDescription(description: string): this;
public setFile(attachment: BufferResolvable | Stream, name?: string): this;
public setFile(attachment: BufferResolvable | Stream | Blob, name?: string): this;
public setName(name: string): this;
public setSpoiler(spoiler?: boolean): this;
public toJSON(): unknown;
Expand Down
9 changes: 7 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1623,7 +1623,7 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==

"@types/node-fetch@^2.5.10", "@types/node-fetch@^2.5.12":
"@types/node-fetch@^2.5.10":
version "2.5.12"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==
Expand Down Expand Up @@ -5899,7 +5899,7 @@ node-fetch@2.6.1:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==

node-fetch@^2.6.1, node-fetch@^2.6.5:
node-fetch@^2.6.5:
version "2.6.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
Expand Down Expand Up @@ -7795,6 +7795,11 @@ underscore@~1.13.1:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.2.tgz#276cea1e8b9722a8dbed0100a407dda572125881"
integrity sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==

undici@^4.12.1:
version "4.12.1"
resolved "https://registry.yarnpkg.com/undici/-/undici-4.12.1.tgz#3a7b5fb12f835a96a65397dd94578464b08d1c27"
integrity sha512-MSfap7YiQJqTPP12C11PFRs9raZuVicDbwsZHTjB0a8+SsCqt7KdUis54f373yf7ZFhJzAkGJLaKm0202OIxHg==

unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
Expand Down