discordjs / discord.js

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

Bot send embed messages twice or even triple times after being not used for a while #10301

Open wisienak opened 1 month ago

wisienak commented 1 month ago

Which package is this bug report for?

discord.js

Issue description

After bot being not used for example 2 hours, after firing (joining from another account) to the discord server, bot send embed message twice. And it only appears when the bot is not used for a while, when i work with bot and restart him often, this problem doesn't appear.

It it matter bot is in screen everytime. No any errors in output.

Code sample

client.on('guildMemberAdd', (member) => {
    const attachment = new AttachmentBuilder('images/welcome.png', { name: 'welcome.png' });
    const embed = new EmbedBuilder()
        .setColor(0x169C9C)
        .setDescription(`Welcolme <@${member.user.id}>! (${member.user.username})\nSome text!\nSome text.\nSome text:\n<#${process.env.RULES_CHANNEL}>\n<#${process.env.TICKETS_CHANNEL}>`)
        .setThumbnail(`https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}.png`)
        .setImage('attachment://welcome.png');

    const channel = member.guild.channels.cache.get(process.env.WELCOME_CHANNEL);
    channel.send({ embeds: [embed], files: [attachment] });
});

Versions

node.js version: v20.13.1 discord.js version: 14.15.2

OS version: Ubuntu 20.04 LTS x86_64 (this problem also appears on my PC when i host it locally)

Issue priority

High (immediate attention needed)

Which partials do you have configured?

No Partials

Which gateway intents are you subscribing to?

Guilds, GuildMembers, GuildMessages, GuildMessageReactions, MessageContent

I have tested this issue on a development release

No response

wisienak commented 1 month ago

During my observation

I added to the event a console.log message to debug how many this event is firing, but i only got one log but 3 messages on channel. Where can be problem?

almostSouji commented 1 month ago

after that time, does the event always fire X times? if so, that sounds like the code attaching a listener is called multiple times over the session, which would cause the event callback to be execute X times, where X is the amount of listeners attached. you can check for that with console.log(client.listenerCount("guildMemberAdd"))

wisienak commented 1 month ago

after that time, does the event always fire X times? if so, that sounds like the code attaching a listener is called multiple times over the session, which would cause the event callback to be execute X times, where X is the amount of listeners attached. you can check for that with console.log(client.listenerCount("guildMemberAdd"))

everytime event got fired 1 time, and this method shows 1 after debug

kyranet commented 1 month ago

For reference, discord.js retries requests (by default, up to 3 times ^1): https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/handlers/Shared.ts#L77-L84

But only on timeout (by default 15 seconds^2) and ECONNRESET[^3]: https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/utils/utils.ts#L86-L97

Since ECONNRESET implies Discord didn't process the request, this can then only mean a timeout, specifically, that your application has sent the request correctly, but due to Internet's nature, the response can fail make its way back to your server, making your app unable acknowledge the response, timing out as a result.

To work around this, we have two options:

Setting options.rest.timeout to change the amount of time discord.js will wait before attempting a retry. This may increase (slightly) the chances of receiving a request, but it will also block any subsequent requests during the duration. For 3 retries at 15 seconds, a request can block for as long as 45 seconds.

Another solution, released by Discord rather recently, is to set enforceNonce to true and generate a random nonce. The combination of the two fields allows Discord to deduplicate the request and therefore even if the library successfully sent 3 requests, Discord would take the first and ignore the rest, sending only one message.

To generate a random nonce reliably, you can use SnowflakeUtil:

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

[^3]: The ECONRESET error means that the server unexpectedly closed the connection and the request to the server was not fulfilled.

wisienak commented 1 month ago

For reference, discord.js retries requests (by default, up to 3 times 1):

https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/handlers/Shared.ts#L77-L84

But only on timeout (by default 15 seconds2) and ECONNRESET3:

https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/utils/utils.ts#L86-L97

Since ECONNRESET implies Discord didn't process the request, this can then only mean a timeout, specifically, that your application has sent the request correctly, but due to Internet's nature, the response can fail make its way back to your server, making your app unable acknowledge the response, timing out as a result.

To work around this, we have two options:

Setting options.rest.timeout to change the amount of time discord.js will wait before attempting a retry. This may increase (slightly) the chances of receiving a request, but it will also block any subsequent requests during the duration. For 3 retries at 15 seconds, a request can block for as long as 45 seconds.

Another solution, released by Discord rather recently, is to set enforceNonce to true and generate a random nonce. The combination of the two fields allows Discord to deduplicate the request and therefore even if the library successfully sent 3 requests, Discord would take the first and ignore the rest, sending only one message.

To generate a random nonce reliably, you can use SnowflakeUtil:

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

Footnotes

  1. https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/utils/constants.ts#L24
  2. https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/utils/constants.ts#L25
  3. The ECONRESET error means that the server unexpectedly closed the connection and the request to the server was not fulfilled.

After add snowflakeutil

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

this problem still appears

Jiralite commented 1 month ago

Show how you implemented this nonce into your code please.

wisienak commented 1 month ago

like this

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();
Jiralite commented 1 month ago

You said that already.

How did you implement that into your code? What did you do with that variable? Where did you put it? Etc.

wisienak commented 1 month ago

i just use require to SnowflakeUtil and create this nonce variable with generate method inside index.js thats it

tipakA commented 1 month ago

So you aren't actually using that variable anywhere?

wisienak commented 1 month ago

oh sorry, i read kyranet comment again and i did mistake, i need to enable enforceNonce and use nonce generated string to use this option recommended by discord.

wisienak commented 1 month ago

ok so i implemented it like this

require('dotenv').config();
const { EmbedBuilder, AttachmentBuilder, SnowflakeUtil } = require('discord.js');

module.exports = (member) => {
    const attachment = new AttachmentBuilder('images/welcome.png', { name: 'welcome.png' });
    const embed = new EmbedBuilder()
        .setColor(0x169C9C)
        .setDescription(`Welcome<@${member.user.id}>! (${member.user.username})\nSome text!\nSome text.\nSome text:\n<#${process.env.RULES_CHANNEL}>\n<#${process.env.TICKETS_CHANNEL}>`)
        .setThumbnail(`https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}.png`)
        .setImage('attachment://welcome.png');

    const nonce = SnowflakeUtil.generate();
    const channel = member.guild.channels.cache.get(process.env.WELCOME_CHANNEL);
    channel.send({ embeds: [embed], files: [attachment], enforceNonce: true, nonce: nonce });
};

i think it should work now

wisienak commented 1 month ago

huh something dont work

/home/bot2/node_modules/discord.js/src/structures/MessagePayload.js:132
        throw new DiscordjsRangeError(ErrorCodes.MessageNonceType);
              ^

RangeError [MessageNonceType]: Message nonce must be an integer or a string.
}
Qjuh commented 1 month ago

Because SnowflakeUtil.generate() gives you a BigInt. You‘d need to .toString() it when passing in your send.

wisienak commented 1 month ago

Because SnowflakeUtil.generate() gives you a BigInt. You‘d need to .toString() it when passing in your send.

yes i did it now, thanks for reply