Search code examples
javascriptnode.jsdiscord.jsbotsnode-sqlite3

Discord.js | Bot's role reactions post does not continue to work between sessions


I'm trying to set up a /roles command for my Discord bot. The goal is to have the bot post an embed with some automated self-reactions that correspond to self-assignable roles identified in the embed. When users click on the reactions, the corresponding role is added. This part works well.

However, that sort of thing only works same session.

To fight against that and maintain a working role reactions embed between bot sessions, I have a sqlite3 database that stores users' reactions as they're added. This way, when the bot goes down and comes back up again, I can fetch the data about who has reacted and with what, so that if they come back and remove their reaction, it should remove the role.

But that's not happening, which is the problem. When a user removes a reaction for the first time after the bot restarts, it does not remove their role. As a beginner, I am still trying to identify my error.

Below is all of the relevant code that I hope will help someone help me to identify my error...


This is the relevant code from my ready.js event that loads the saved embed's reaction collectors:

        try {
            // Fetch members for all guilds to initialize member cache
            await Promise.all(client.guilds.cache.map(guild => guild.members.fetch()));

            // Load saved embeds and set up reaction collectors
            const savedEmbeds = await Embed.findAll();
            for (const savedEmbed of savedEmbeds) {
                const { id, messageId, channelId, maxReactions } = savedEmbed;
                const channel = await client.channels.fetch(channelId);
                if (!channel) {
                    continue;
                }
                const message = await channel.messages.fetch(messageId);
                if (!message) {
                    continue;
                }
                await setupReactionCollectors(message, client, maxReactions, id);
            }
        } catch (error) {
            console.error('[ready.js] Error loading saved embeds and reactions:', error);
        }

Next is the relevant code from my roleManager.js util that attempts to sync reactions from the database upon startup:

// Sync initial reactions and roles from Discord with the database
async function syncInitialReactions(message, client, maxReactions, embedId) {
    const reactions = message.reactions.cache;
    console.log(`[syncInitialReactions] Syncing initial reactions for message ${message.id}`);
    for (const [emoji, reaction] of reactions) {
        const users = await reaction.users.fetch();
        console.log(`[syncInitialReactions] Users fetched for emoji ${emoji.toString()} on message ${message.id}: ${Array.from(users.keys()).join(', ')}`);
        for (const user of users.values()) {
            if (!user.bot) {
                console.log(`[syncInitialReactions] Checking reaction ${emoji} from user ${user.tag} on message ${message.id}`);

                // Check if this reaction is already in the database
                const existingReaction = await Reaction.findOne({
                    where: {
                        embedId: embedId,
                        userId: user.id,
                        emoji: emoji.toString(),
                    },
                });

                if (!existingReaction) {
                    console.log(`[syncInitialReactions] Database does not contain reaction ${emoji} for user ${user.tag} on message ${message.id}. Adding to database and assigning role.`);

                    // Assign the role if not already assigned
                    await handleRoleAssignment(reaction, user, 'add', client, maxReactions, embedId, true);

                    // Add the reaction to the database
                    await Reaction.create({
                        embedId: embedId,
                        userId: user.id,
                        emoji: emoji.toString(),
                        roleId: reaction.message.guild.roles.cache.find(r => reaction.message.embeds[0]?.fields?.find(field => field.value.includes(emoji.toString()))?.value.includes(`<@&${r.id}>`))?.id,
                    });
                } else {
                    console.log(`[syncInitialReactions] Reaction ${emoji} from user ${user.tag} on message ${message.id} already recorded in database`);
                }
            }
        }

        // Remove reactions from the database that are no longer present on Discord
        const dbReactions = await Reaction.findAll({
            where: {
                embedId: embedId,
                emoji: emoji.toString(),
            },
        });

        for (const dbReaction of dbReactions) {
            if (!users.has(dbReaction.userId)) {
                console.log(`[syncInitialReactions] Reaction ${dbReaction.emoji} from user ${dbReaction.userId} on message ${message.id} missing on Discord. Removing from database and role.`);
                await handleRoleAssignment(reaction, { id: dbReaction.userId, tag: 'unknown' }, 'remove', client, maxReactions, embedId, true);
                await Reaction.destroy({
                    where: {
                        id: dbReaction.id,
                    },
                });
            }
        }
    }
}

This is the relevant code from the reactionManager.js which sets up the reaction collectors:

// ./utils/reactionManager.js
const { handleRoleAssignment, syncInitialReactions } = require('./roleManager');

// Setup reaction collectors for a given message
async function setupReactionCollectors(message, client, maxReactions, embedId) {
    const filter = (reaction, user) => !user.bot;
    const collector = message.createReactionCollector({ filter, dispose: true });

    collector.on('collect', async (reaction, user) => {
        console.log(`[reactionManager] Reaction collected on message ${message.id} by user ${user.tag}`);
        await handleRoleAssignment(reaction, user, 'add', client, maxReactions, embedId);
    });

    collector.on('remove', async (reaction, user) => {
        console.log(`[reactionManager] Reaction removed on message ${message.id} by user ${user.tag}`);
        await handleRoleAssignment(reaction, user, 'remove', client, maxReactions, embedId);
    });

    console.log(`[reactionManager] Collectors set up for message ${message.id}`);

    // Sync initial reactions only after collectors are set up
    await syncInitialReactions(message, client, maxReactions, embedId);
}

module.exports = {
    setupReactionCollectors,
};

Solution

  • On Discord.JS collectors like the ones that you have used message.createReactionCollector are storred in the execution memory so when you restart the code they are bound to well not work anymore.

    If you want to have a Event handler that works even on a restart then I would suggest you to use messageReactionAdd and messageReactionRemove client events. For your use the code could be like

    client.on("messageReactionAdd", async (reaction, user) => {
      await handleRoleAssignment(reaction, user, 'add', client, maxReactions, message.id);
    })
    client.on("messageReactionRemove", async (reaction, user) => {
      await handleRoleAssignment(reaction, user, 'remove', client, maxReactions, message.id);
    })
    

    NOTE: This will only work on cached messages!!