Am attempting to follow a tutorial to use DiscordJS with Typescript, and the tutorial I was following also covers the use of Inversify for dependancy injection. https://www.cobeisfresh.com/blog/how-to-build-a-free-discord-bot-with-node-js
I've hit an error when trying to get the bot to connect to discord.
Any assistance or suggestions would be greatly appreciated :) I've been unable to find anything online and I've been told its apparently "not a DJS error", so I have no idea where to look to figure out whats going on.
I have a bot working written in JS which uses commonjs, but this is my first attempt trying to get a discord bot working with typescript and webpack. That makes me think maybe it's something to do with how webpack/typescript is compiling the code for node??
The error I'm getting is this (the /.../ is just me shortening my dev directory url):
HTTPError [TypeError]: fetch is not a function
at RequestHandler.execute (/home/.../discord-bot-ts/dist/index.js:16023:15)
at RequestHandler.execute (/home/.../discord-bot-ts/dist/index.js:16027:19)
at RequestHandler.push (/home/.../discord-bot-ts/dist/index.js:15873:25)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async WebSocketManager.connect (/home/.../discord-bot-ts/dist/index.js:7799:9)
at async Client.login (/home/.../discord-bot-ts/dist/index.js:4850:7) {
code: 500,
method: 'get',
path: '/gateway/bot',
requestData: { json: undefined, files: [] }
}
My code is as follows:
// main.ts (entry point)
import 'dotenv/config'; // process.env.<key>
import container from './core/inversify.config';
import TYPES from './core/inversify.types';
import Bot from './core/Bot';
import Logger from './helpers/Logger';
const bot = container.get<Bot>(TYPES.Bot);
const logger = container.get<Logger>(TYPES.Logger);
bot.init().then(() => {
logger.log('stuff')
})
// ./core/inversify.config.ts
import "reflect-metadata";
import { Container } from "inversify";
import TYPES from "./inversify.types";
import Client from './Client';
import Bot from './Bot';
import Logger from "../helpers/Logger";
const container = new Container();
container.bind<Client>(TYPES.Client).toConstantValue(new Client());
container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope();
container.bind<string>(TYPES.Token).toConstantValue(process.env.TOKEN);
container.bind<Logger>(TYPES.Logger).to(Logger).inSingletonScope();
export default container;
// ./core/inversify.types.ts
export default {
Client: Symbol.for('client'),
Bot: Symbol.for('bot'),
Token: Symbol.for('token'),
Logger: Symbol.for('logger'),
};
// ./core/Client.ts
import { Client as DJSClient, Intents } from "discord.js";
import { injectable } from "inversify";
@injectable()
class Client extends DJSClient {
constructor() {
super({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_MESSAGES,
Intents.FLAGS.DIRECT_MESSAGES,
Intents.FLAGS.GUILD_MEMBERS,
Intents.FLAGS.GUILD_BANS,
Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
Intents.FLAGS.GUILD_INVITES,
Intents.FLAGS.GUILD_VOICE_STATES,
Intents.FLAGS.GUILD_PRESENCES,
Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
Intents.FLAGS.GUILD_MESSAGE_TYPING,
Intents.FLAGS.DIRECT_MESSAGE_REACTIONS,
Intents.FLAGS.DIRECT_MESSAGE_TYPING,
],
partials: ["CHANNEL", "MESSAGE", "REACTION", "USER", "GUILD_MEMBER"],
presence: {
activities: [
{
name: "The Network",
type: "LISTENING",
},
],
},
});
}
}
export default Client;
// ./core/Bot.ts
import { inject, injectable } from "inversify";
import Client from './Client';
import TYPES from './inversify.types';
@injectable()
class DiscordBot {
private client: Client;
private readonly token: string;
constructor(
@inject(TYPES.Client) client: Client,
@inject(TYPES.Token) token: string
){
this.client = client;
this.token = token;
}
init(): Promise<string> {
return this.client.login(this.token);
}
}
export default DiscordBot;
// ./helpers/Logger.ts
import { injectable } from "inversify";
@injectable()
class Logger {
log(message: string){
console.log(message);
}
}
export default Logger;
My webpack and ts configs:
const webpack = require("webpack");
const path = require("path");
const ESLintPlugin = require("eslint-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const buildPath = path.resolve(__dirname, "../dist");
const bot = {
entry: "./main.ts",
target: "node",
module: {
rules: [
{
test: /\.tsx?$/,
use: ["ts-loader"],
exclude: /node_modules/,
},
],
},
plugins: [
new ESLintPlugin(),
new CleanWebpackPlugin(),
],
// below because otherwise I get error Module not found: Error: Can't resolve 'utf-8-validate' etc
externals: ['utf-8-validate', 'bufferutil', 'zlib-sync', 'erlpack'],
optimization: {
minimize: false,
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "index.js",
path: path.resolve(buildPath),
},
};
module.exports = bot;
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": ["types/*"]
},
"outDir": "./",
"noImplicitAny": false,
"module": "commonjs",
"target": "es6",
"allowJs": false,
"lib": ["es2017", "DOM"],
"types": ["node", "reflect-metadata"],
"typeRoots": [
"node_modules/@types"
],
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": ["./**/*"],
"exclude": ["**/node_modules", "**/__tests__/*", "**/tests/"]
}
The compiled code at line 4850 in the error is this:
/**
* Logs the client in, establishing a WebSocket connection to Discord.
* @param {string} [token=this.token] Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token');
*/
async login(token = this.token) {
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
this.emit(
Events.DEBUG,
`Provided token: ${token
.split('.')
.map((val, i) => (i > 1 ? val.replace(/./g, '*') : val))
.join('.')}`,
);
if (this.options.presence) {
this.options.ws.presence = this.presence._parse(this.options.presence);
}
this.emit(Events.DEBUG, 'Preparing to connect to the gateway...');
try {
await this.ws.connect(); // line 4850
return this.token;
} catch (error) {
this.destroy();
throw error;
}
}
Edited to add code at line 16023 per comment request
async execute(request) {
/*
* After calculations have been done, pre-emptively stop further requests
* Potentially loop until this task can run if e.g. the global rate limit is hit twice
*/
while (this.limited) {
const isGlobal = this.globalLimited;
let limit, timeout, delayPromise;
if (isGlobal) {
// Set the variables based on the global rate limit
limit = this.manager.globalLimit;
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
} else {
// Set the variables based on the route-specific rate limit
limit = this.limit;
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
}
if (this.manager.client.listenerCount(RATE_LIMIT)) {
/**
* Emitted when the client hits a rate limit while making a request
* @event BaseClient#rateLimit
* @param {RateLimitData} rateLimitData Object containing the rate limit info
*/
this.manager.client.emit(RATE_LIMIT, {
timeout,
limit,
method: request.method,
path: request.path,
route: request.route,
global: isGlobal,
});
}
if (isGlobal) {
// If this is the first task to reach the global timeout, set the global delay
if (!this.manager.globalDelay) {
// The global delay function should clear the global delay state when it is resolved
this.manager.globalDelay = this.globalDelayFor(timeout);
}
delayPromise = this.manager.globalDelay;
} else {
delayPromise = sleep(timeout);
}
// Determine whether a RateLimitError should be thrown
await this.onRateLimit(request, limit, timeout, isGlobal); // eslint-disable-line no-await-in-loop
// Wait for the timeout to expire in order to avoid an actual 429
await delayPromise; // eslint-disable-line no-await-in-loop
}
// As the request goes out, update the global usage information
if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
this.manager.globalReset = Date.now() + 1_000;
this.manager.globalRemaining = this.manager.globalLimit;
}
this.manager.globalRemaining--;
/**
* Represents a request that will or has been made to the Discord API
* @typedef {Object} APIRequest
* @property {HTTPMethod} method The HTTP method used in this request
* @property {string} path The full path used to make the request
* @property {string} route The API route identifying the rate limit for this request
* @property {Object} options Additional options for this request
* @property {number} retries The number of times this request has been attempted
*/
if (this.manager.client.listenerCount(API_REQUEST)) {
/**
* Emitted before every API request.
* This event can emit several times for the same request, e.g. when hitting a rate limit.
* <info>This is an informational event that is emitted quite frequently,
* it is highly recommended to check `request.path` to filter the data.</info>
* @event BaseClient#apiRequest
* @param {APIRequest} request The request that is about to be sent
*/
this.manager.client.emit(API_REQUEST, {
method: request.method,
path: request.path,
route: request.route,
options: request.options,
retries: request.retries,
});
}
// Perform the request
let res;
try {
res = await request.make();
} catch (error) {
// Retry the specified number of times for request abortions
if (request.retries === this.manager.client.options.retryLimit) {
// THIS IS LINE 16023
throw new HTTPError(error.message, error.constructor.name, error.status, request);
}
request.retries++;
// THIS IS LINE 16027
return this.execute(request);
}
if (this.manager.client.listenerCount(API_RESPONSE)) {
/**
* Emitted after every API request has received a response.
* This event does not necessarily correlate to completion of the request, e.g. when hitting a rate limit.
* <info>This is an informational event that is emitted quite frequently,
* it is highly recommended to check `request.path` to filter the data.</info>
* @event BaseClient#apiResponse
* @param {APIRequest} request The request that triggered this response
* @param {Response} response The response received from the Discord API
*/
this.manager.client.emit(
API_RESPONSE,
{
method: request.method,
path: request.path,
route: request.route,
options: request.options,
retries: request.retries,
},
res.clone(),
);
}
let sublimitTimeout;
if (res.headers) {
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');
const resetAfter = res.headers.get('x-ratelimit-reset-after');
this.limit = limit ? Number(limit) : Infinity;
this.remaining = remaining ? Number(remaining) : 1;
this.reset = reset || resetAfter ? calculateReset(reset, resetAfter, serverDate) : Date.now();
// https://github.com/discord/discord-api-docs/issues/182
if (!resetAfter && request.route.includes('reactions')) {
this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
}
// Handle retryAfter, which means we have actually hit a rate limit
let retryAfter = res.headers.get('retry-after');
retryAfter = retryAfter ? Number(retryAfter) * 1_000 : -1;
if (retryAfter > 0) {
// If the global rate limit header is set, that means we hit the global rate limit
if (res.headers.get('x-ratelimit-global')) {
this.manager.globalRemaining = 0;
this.manager.globalReset = Date.now() + retryAfter;
} else if (!this.localLimited) {
/*
* This is a sublimit (e.g. 2 channel name changes/10 minutes) since the headers don't indicate a
* route-wide rate limit. Don't update remaining or reset to avoid rate limiting the whole
* endpoint, just set a reset time on the request itself to avoid retrying too soon.
*/
sublimitTimeout = retryAfter;
}
}
}
// Count the invalid requests
if (res.status === 401 || res.status === 403 || res.status === 429) {
if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
invalidCountResetTime = Date.now() + 1_000 * 60 * 10;
invalidCount = 0;
}
invalidCount++;
const emitInvalid =
this.manager.client.listenerCount(INVALID_REQUEST_WARNING) &&
this.manager.client.options.invalidRequestWarningInterval > 0 &&
invalidCount % this.manager.client.options.invalidRequestWarningInterval === 0;
if (emitInvalid) {
/**
* @typedef {Object} InvalidRequestWarningData
* @property {number} count Number of invalid requests that have been made in the window
* @property {number} remainingTime Time in ms remaining before the count resets
*/
/**
* Emitted periodically when the process sends invalid requests to let users avoid the
* 10k invalid requests in 10 minutes threshold that causes a ban
* @event BaseClient#invalidRequestWarning
* @param {InvalidRequestWarningData} invalidRequestWarningData Object containing the invalid request info
*/
this.manager.client.emit(INVALID_REQUEST_WARNING, {
count: invalidCount,
remainingTime: invalidCountResetTime - Date.now(),
});
}
}
// Handle 2xx and 3xx responses
if (res.ok) {
// Nothing wrong with the request, proceed with the next one
return parseResponse(res);
}
// Handle 4xx responses
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
const isGlobal = this.globalLimited;
let limit, timeout;
if (isGlobal) {
// Set the variables based on the global rate limit
limit = this.manager.globalLimit;
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
} else {
// Set the variables based on the route-specific rate limit
limit = this.limit;
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
}
this.manager.client.emit(
DEBUG,
`Hit a 429 while executing a request.
Global : ${isGlobal}
Method : ${request.method}
Path : ${request.path}
Route : ${request.route}
Limit : ${limit}
Timeout : ${timeout}ms
Sublimit: ${sublimitTimeout ? `${sublimitTimeout}ms` : 'None'}`,
);
await this.onRateLimit(request, limit, timeout, isGlobal);
// If caused by a sublimit, wait it out here so other requests on the route can be handled
if (sublimitTimeout) {
await sleep(sublimitTimeout);
}
return this.execute(request);
}
// Handle possible malformed requests
let data;
try {
data = await parseResponse(res);
} catch (err) {
throw new HTTPError(err.message, err.constructor.name, err.status, request);
}
throw new DiscordAPIError(data, res.status, request);
}
// Handle 5xx responses
if (res.status >= 500 && res.status < 600) {
// 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);
}
request.retries++;
return this.execute(request);
}
// Fallback in the rare case a status code outside the range 200..=599 is returned
return null;
}
Edit 2: Tested stripped back code, I get the same error with the following code in my main.ts file
import 'dotenv/config';
import { Client, Intents } from "discord.js";
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
client.once('ready', () => {
console.log('Ready!');
});
client.login(process.env.TOKEN);