discordjs / discord.js

A powerful JavaScript library for interacting with the Discord API
https://discord.js.org
Apache License 2.0
25.39k stars 3.97k forks source link

discord.js appears to not be handling rate limits like it should #8999

Closed kettle-7 closed 1 year ago

kettle-7 commented 1 year ago

Which package is this bug report for?

discord.js

Issue description

I can't really get a minimum reproducible example because I spent all of today trying to slim the code down and fix the issue but to no avail. I have a fairly big (but not huge) bot to link a Discord server to a modded Minecraft server's chat. Originally I was getting request time-out errors even after extending the timeout to a minute in the Client options, so I removed a lot of the messages getting sent and am now getting the Cloudflare block, I've had it three times now today and can't use Discord until probably tomorrow. I would prefer to avoid too much trial and error because I hear you can get a permanent ban. Anywho, during normal operations the bot stops sending messages (although it appears to continue functioning as usual) and on my laptop and phone I become unable to use Discord anything. The bot isn't used very much and I don't see how it manages to send enough requests to trigger the rate limit, and even if it does I'm pretty sure discord.js is supposed to handle the event. So the issue seems to lie in either Discord itself (in which case I can create a support ticket) or in discord.js. Again, example is not very minimal because I can't really test without risking permanently getting blocked from Discord which is something I would rather didn't happen.

Code sample

// webthing: a web thing

var cwd = require('process').cwd;
var fs = require('fs');
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
const http = require('http');
var mcp = {killed: true}; // effectively a vegetable process
var serup = "";
var serchosen = "None";
var ngrokp; // for port forwarding
var nonecount = 0;

var conf = JSON.parse(fs.readFileSync('./bot.conf'));

const { Client, GatewayIntentBits, WebhookClient, EmbedBuilder, SlashCommandBuilder } = require('discord.js');
const client = new Client({ intents: [
    GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers
], restRequestTimeout: 15000 });
// NOTE: I was using 60000 and it still didn't work

var wantRestart = false;
var cmdChannels = [];
var logouts = [];
var imgurl = "";
var players = 0;
var logs = [];
var outs = [];
var ins = [];
var selser;

function serstrt(message) {
    players = 0;
    if (!fs.existsSync(cwd()+"/"+serchosen+"/")) {
        message.reply("That server does not exist. It may have been deleted or renamed.");
        return;
    }
    console.log("sestrt");
    for (let channel in outs) {
        new WebhookClient({url: outs[channel]}).send({
            avatarURL: imgurl,
            embeds: [new EmbedBuilder().setDescription("Starting the server...")],
            username: serchosen
        });
    }
    mcp = spawn (conf.javaPath, ['-Xms4096M', '-Xmx4096M', '-jar', 'server.jar', 'nogui', 'pause'], {cwd:cwd()+"/"+serchosen});
    mcp.stdin.setEncoding('utf-8');
    mcp.stdout.on('data', (edata) => {
        console.log(edata.toString());
        if (edata.toString().includes("Preparing spawn area")) return;
        var ldata = edata.toString().split('\n');
        for (var ndata in ldata) {
            let data = ldata[ndata];
            if (data.replace(/[ \n]/g, '') == "") { return; }
            for (let channel in logouts) {
                new WebhookClient({url: logouts[channel]}).send(data);
            }
            if (data.includes('[Server thread/INFO]: <')) {
                let parts = data.split(': ');
                delete parts[0];
                let msg = parts.join(': ').replace(': ', '');
                ml = msg.split("");
                let munam = ml.slice(ml.indexOf("<"), ml.indexOf(">") - ml.indexOf("<"));
                ml.splice(ml.indexOf("<"), ml.indexOf(">"));
                msg = ml.reduce((partialSum, a) => partialSum + a, 0);
                if (msg)
                for (let channel in outs) {
                    new WebhookClient({url: outs[channel]}).send({
                        avatarURL: "https://mc-heads.net/avatar/"+munam+".png",
                        content: msg,
                        username: munam
                    });
                }
            }
            else if (data.indexOf(' joined the game') + data.indexOf(' left the game') > -2) {
                // 20mars
                let parts = data.split(': ');
                delete parts[0];
                let msg = parts.join(': ').replace(': ', '');
                var join = false;
                if (msg.indexOf(" joined the game") > -1) {
                    join = true;
                }
                // if somebody is joining then add to the number of players, otherwise subtract
                if (join) {
                    players++;
                }
                else if (data.indexOf(' left the game') > -1){
                    players--;
                }
                // if there's nobody on then prepare to stop the server in two minutes
                if ((data.indexOf(' left the game') > -1) && players < 1) {
                    setTimeout(() => {
                        if (players < 1) {
                            mcp.stdin.write("\nstop\n");
                        }
                    }, 120000)
                }
                if (msg)
                for (let channel in outs) {
                    new WebhookClient({url: outs[channel]}).send({
                        avatarURL: imgurl,
                        content: msg,
                        username: serchosen
                    });
                }
            }
            else if (data.includes('For help, type "help"')) {
                for (let channel in outs) {
                    new WebhookClient({url: outs[channel]}).send({
                        avatarURL: imgurl,
                        embeds: [new EmbedBuilder().setDescription("Server started.")],
                        username: serchosen
                    });
                }
            }
        }
    });
    mcp.on('exit', (code) => {
        for (let channel in outs) {
            new WebhookClient({url: outs[channel]}).send({
                avatarURL: imgurl,
                embeds: [new EmbedBuilder().setDescription("Server closed.")],
                username: serchosen
            });
        }
        mcp.killed = true;
        console.log(`Process exited with exit code ${code}.`);
        if (wantRestart) {
            wantRestart = false;
            process.exit();
        }
        //serstrt(message);
    });
    setTimeout(poshRestart, 21600000); // restart after 6 hours
}

function execute(command, callback){
    exec(command, function(_error, stdout, stderr){ console.error(stderr); callback(stdout); });
};

function poshRestart() {
    mcp.stdin.write('/say The server will be restarting in 30 seconds\n');
    for (let channel in outs) {
        new WebhookClient({url: outs[channel]}).send({
            avatarURL: imgurl,
            embeds: [new EmbedBuilder().setTitle(serchosen).setDescription("The server will be restarting in 30 seconds")],
            username: "mincefart"
        });
    }
    setTimeout(() => {
        mcp.stdin.write('stop');
        wantRestart = true;
        setTimeout(() => {
            if(wantRestart) {
                // we still haven't restarted yet
                wantRestart = false;
                mcp.kill("SIGKILL");
            }
        }, 300000);
    }, 300000);
}

client.on('ready', () => {
    var lcmd = {
        data: new SlashCommandBuilder()
                .setName('leaderboard')
                .setDescription('You think you have many legs ? Find out who\'s had the most legs, and get good.')
    }

    client.application.commands.create(lcmd.data);
    for (let i = 0; i < conf.servers.length; i++) {
        let ser = conf.servers[i];
        if (!fs.existsSync(cwd()+"/"+ser.dir)) {
            client.channels.fetch(conf.serInfoChannel).then(function(channel) {
                channel.send("Warning (or error if you prefer): the server "+ser.dir+" is mis\
sing the folder with all the important files (like the server jar etc). I'm not smart eno\
ugh to actually fix it so I'm just gonna ping <@966799868901347348>. Good luck !");
            }, function(error) {throw error;});
            continue;
        }
        else {
            for (let j = 0; j < ser.chats.length; j++) {
                cmdChannels.push(ser.chats[j][1]);
            }
        }
    }

    selser = conf.servers[0];
    serchosen = selser.dir;
    imgurl = selser.imgurl;
    for (let i = 0; i < selser.chats.length; i++) {
        if (!outs.includes(selser.chats[i][0]))
            outs.push(selser.chats[i][0]);
        ins.push(selser.chats[i][1]);
    }
    for (let i = 0; i < selser.logs.length; i++) {
        logs.push(selser.logs[i][1]);
    }
    console.log("The bot is down and runnin' baby!(I'm a pedophile)");
});

client.on("interactionCreate", interaction => {
    if (!interaction.isChatInputCommand()) return;
    if (interaction.commandName != "leaderboard") return;
    let leghs = 0;
    if (conf.legHighScores[interaction.user.id] != undefined) {
        leghs = Math.round(conf.legHighScores[interaction.user.id]);
    }
    let mleghs = 0;
    if (conf.legHighScores[client.user.id] != undefined) {
        mleghs = Math.round(conf.legHighScores[client.user.id]);
    }
    let bottom = "";
    let sortable = [];
    for (let p in conf.legHighScores) {
        sortable.push([p, conf.legHighScores[p]]);
    }

    sortable.sort(function(a, b) {
        return b[1] - a[1];
    });
    for (let o = 0; o < 10 && o < sortable.length; o++) {
        bottom += ("\n" + (o + 1).toString() + ") <@"+sortable[o][0]+"> "+Math.round(sortable[o][1]).toString());
    }
    let top = `

Your leg high score is ${leghs}.
My leg high score is ${mleghs}.

**__Leg Leaderboard__**
`;

    interaction.reply({ embeds: [
        new EmbedBuilder().setTitle("Leg Statistics").setDescription(top + bottom)]
    });
});

client.on("messageCreate", message => {
    if (message.webhookId) return;

    else if (message.toString().toLowerCase() == "legs?") {
        let leghs = 1;
        if (conf.legHighScores[message.author.id] != undefined) {
            leghs = conf.legHighScores[message.author.id];
        }
        let legmax = Math.log(1+(leghs * 1.2));
        let legs = Math.exp(Math.random() * legmax);
        if (Math.random() < 0.5) legs = 0;
        if (Math.round(legs) < 1) legs = 0;
        if (legs > leghs) leghs = legs;
        conf.legHighScores[message.author.id] = leghs;
        fs.writeFileSync("./bot.conf", JSON.stringify(conf, null, 4));
        if (legs) message.reply (("u have "+Math.round(legs.toString())+" legs lol").replace(" 1 legs", " only one leg, good luck balancing"));
        else message.reply ("u don't have legs (get good)");
    }
    else if (message.toString().toLowerCase() == "how many legs?") {
        message.reply("The bot now uses a slash command for more inconsistency. Please use /leaderboard to view the leaderboard. Thanks !");
    }
    if (message.toString().indexOf("<@"+client.user.id.toString()+">") >= 0) {
        message.reply ("ping :angry:");
    }
    var t = message.toString();
    if (t.indexOf("!start") == 0) {
        if (!cmdChannels.includes(message.channelId)) {
            message.reply("this channel is not linked to any servers (get good).");
            return;
        }
        if (!mcp.killed) {
            message.reply("A server is already running (you better go catch it :wink:).");
            return;
        }
        let lits = t.split(" ");
        if (lits.length >= 2) {
            serchosen = lits[1];
            for (let ser in conf.servers) {
                if (ser.dir == serchosen) {
                    outs.length = 0;
                    ins.length = 0;
                    logs.length = 0;
                    selser = ser;
                    imgurl = ser.imgurl;
                    for (let i = 0; i < ser.chats.length; i++) {
                        if (!outs.includes(ser.chats[i][0]))
                            outs.push(ser.chats[i][0]);
                        ins.push(ser.chats[i][1]);
                    }
                    for (let i = 0; i < ser.logs.length; i++) {
                        if (!logouts.includes(ser.logs[i][0]))
                            logouts.push(ser.logs[i][0]);
                        if (!logs.includes(ser.logs[i][1]))
                            logs.push(ser.logs[i][1]);
                    }
                }
            }
        }
        if (!ins.includes(message.channelId)) {
            // basically this channel doesn't have permission to start this server
            for (let i = 0; i < conf.servers.length; i++) {
                let ser = conf.servers[i];
                for (let c in ser.chats) {
                    if (c == message.channelId) {
                        outs.length = 0;
                        ins.length = 0;
                        logs.length = 0;
                        serchosen = ser.dir;
                        selser = ser;
                        imgurl = ser.imgurl;
                        for (let i = 0; i < ser.chats.length; i++) {
                            if (!outs.includes(ser.chats[i][0]))
                                outs.push(ser.chats[i][0]);
                            ins.push(ser.chats[i][1]);
                        }
                        for (let i = 0; i < ser.logs.length; i++) {
                            if (!logouts.includes(ser.logs[i][0]))
                                logouts.push(ser.logs[i][0]);
                            if (!logs.includes(ser.logs[i][1]))
                                logs.push(ser.logs[i][1]);
                        }
                        message.reply("This channel isn't linked to that server so I'm starting "+ser.dir+".");
                    }
                }
            }
        }
        else {
            let existancementousness = false;
            for (let ser in conf.servers) { // it isn't for some reason
                ser = conf.servers[ser];
                if (ser.dir == serchosen) { // ser.dir is undefined
                    existancementousness = true;
                    serchosen = ser.dir;
                    outs.length = 0;
                    ins.length = 0;
                    logs.length = 0;
                    selser = ser;
                    imgurl = ser.imgurl;
                    for (let i = 0; i < ser.chats.length; i++) {
                        if (!outs.includes(ser.chats[i][0]))
                            outs.push(ser.chats[i][0]);
                        ins.push(ser.chats[i][1]);
                    }
                    for (let i = 0; i < ser.logs.length; i++) {
                        if (!logouts.includes(ser.logs[i][0]))
                            logouts.push(ser.logs[i][0]);
                        if (!logs.includes(ser.logs[i][1]))
                            logs.push(ser.logs[i][1]);
                    }
                }
            }
            if (!existancementousness) {
                message.reply("That isn't a server, don't see a list of servers.");
                return;
            }
        }
        serstrt(message);
        return;
    }
    if (ins.includes(message.channelId)) { // does not work on internet explorer :O)
        if (t.indexOf("!stop") == 0) {     // i hope the bot isn't running in internet explorer
            poshRestart();                 // but we will never know until this code crashes
        }
        else if (!mcp.killed) {
            var a = message.member.nickname;
            if(!a) a = message.author.username;
            var d = JSON.stringify([
                {"text": "<"+a+" (Discord)> ", "color":"blue"},
                {"text": t,"color":"white"}
            ]);
            mcp.stdin.write("tellraw @a "+d + "\n");
        }
    }
    if (message.author.id == client.user.id) return;
    if (logs.includes(message.channelId)) {
        if (t.indexOf("!forcestop") == 0) {
            let lits = t.split(" ");
            if (lits.length >= 2) {
                serchosen = lits[1];
            }
            mcp.kill("SIGTERM");
            return;
        }
        else if (t.indexOf("!forcerstop") == 0) {
            let lits = t.split(" ");
            if (lits.length >= 2) {
                serchosen = lits[1];
            }
            mcp.kill("SIGKILL");
            return;
        }
        else if ((t.indexOf("!start") == 0) || (t.indexOf("!restart") == 0)) {
            let lits = t.split(" ");
            if (lits.length >= 2) {
                serchosen = lits[1];
            }
            if (!mcp.killed) {
                message.reply("The server is already running.\n*You better go catch it*");
                return;
            }
            serstrt(message);
            return;
        }
        else if (t.indexOf("!stop") == 0) {
            let lits = t.split(" ");
            if (lits.length >= 2) {
                serchosen = lits[1];
            }
            poshRestart();
        }
        else if (!mcp.killed) {
            mcp.stdin.write(t + "\n");
        }
        else {
            message.reply("The server is currently down.")
        }
    }
});

client.login(conf.token);

Package version

14.7.1

Node.js version

19.0.1

Operating system

Windows 11 22H2

Priority this issue should have

Medium (should be fixed soon)

Which partials do you have configured?

No Partials

Which gateway intents are you subscribing to?

Guilds, GuildMembers, GuildMessages, MessageContent

I have tested this issue on a development release

No response

kettle-7 commented 1 year ago

Worth adding, it must be something very specific in the code if nobody else has reported it

kyranet commented 1 year ago

From a first (and quick) glance, it seems your issue is most likely caused because you recreate WebhookClients over and over, those extend BaseClient which holds the REST manager (and its queue).

By recreating the queues all the time, you lose the information from previous ones, and as such, you're not queueing, but bursting single-request queues, that's why you get so many 429 and why you eventually get CF banned.

Try to reuse them, and likely then you will stop getting ratelimited.

kettle-7 commented 1 year ago

From a first (and quick) glance, it seems your issue is most likely caused because you recreate WebhookClients over and over, those extend BaseClient which holds the REST manager (and its queue).

By recreating the queues all the time, you lose the information from previous ones, and as such, you're not queueing, but bursting single-request queues, that's why you get so many 429 and why you eventually get CF banned.

Try to reuse them, and likely then you will stop getting ratelimited.

ahh right, i need to find a better way to store them (i had an object with an entry for each url but the garbage collector seemed to be cleaning them up too soon)

BillyDotJs commented 1 year ago

How the fuck do I stop these emails every 10 minutes

On Sun, 1 Jan 2023 at 11:31 AM, upside-down guy @.***> wrote:

From a first (and quick) glance, it seems your issue is most likely caused because you recreate WebhookClients over and over, those extend BaseClient which holds the REST manager (and its queue).

By recreating the queues all the time, you lose the information from previous ones, and as such, you're not queueing, but bursting single-request queues, that's why you get so many 429 and why you eventually get CF banned.

Try to reuse them, and likely then you will stop getting ratelimited.

ahh right, i need to find a better way to store them (i had an object with an entry for each url but the garbage collector seemed to be cleaning them up too soon)

— Reply to this email directly, view it on GitHub https://github.com/discordjs/discord.js/issues/8999#issuecomment-1368288152, or unsubscribe https://github.com/notifications/unsubscribe-auth/AVIZ6JB3PUAR3KDU7CNAQDLWQCX3HANCNFSM6AAAAAATNPEDQU . You are receiving this because you are subscribed to this thread.Message ID: @.***>

kyranet commented 1 year ago

You can have a Map at the top level, or use Collection from discord.js so you can call ensure, that way you can get or insert the WebhookClient.

Just don't recreate the cache every time it's called and you're fine.

kettle-7 commented 1 year ago

How the fuck do I stop these emails every 10 minutes On Sun, 1 Jan 2023 at 11:31 AM, upside-down guy @.> wrote: From a first (and quick) glance, it seems your issue is most likely caused because you recreate WebhookClients over and over, those extend BaseClient which holds the REST manager (and its queue). By recreating the queues all the time, you lose the information from previous ones, and as such, you're not queueing, but bursting single-request queues, that's why you get so many 429 and why you eventually get CF banned. Try to reuse them, and likely then you will stop getting ratelimited. ahh right, i need to find a better way to store them (i had an object with an entry for each url but the garbage collector seemed to be cleaning them up too soon) — Reply to this email directly, view it on GitHub <#8999 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AVIZ6JB3PUAR3KDU7CNAQDLWQCX3HANCNFSM6AAAAAATNPEDQU . You are receiving this because you are subscribed to this thread.Message ID: @.>

Go to the home page of the repository, click Unwatch and then either Participating / Mentioned or Never

kettle-7 commented 1 year ago

I'm confused why that message sent twice

kettle-7 commented 1 year ago

You can have a Map at the top level, or use Collection from discord.js so you can call ensure, that way you can get or insert the WebhookClient.

Just don't recreate the cache every time it's called and you're fine.

Yeah originally I did it like that and the objects inside the map became all null so I made the map a property of the Client object and it works perfectly now. Thanks for the help !