SmallFry
SmallFry

Reputation: 1

How to serialize discord API button interactions

I am making a JS discord bot that when a command is run, an embed is displayed with a button row underneath the embed. Im having some problems when multiple users run the same command or when the same user runs the command twice in a row; discord throws an Unknown Interaction.

// utilities/buttonHandler.js VERSION 5
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const crypto = require('crypto'); // To generate unique IDs

const activeCollectors = new Map(); // Map to track active collectors

function generateUniqueId(prefix) {
    return `${prefix}_${crypto.randomUUID()}`;
}

async function handleButtonInteraction(interaction, customId, user1, user2, client) {
    // Disable all buttons immediately after the interaction
    const buttons = [
        new ButtonBuilder().setCustomId(generateUniqueId('createCase')).setLabel('Create Case').setStyle(ButtonStyle.Success).setDisabled(true),
        new ButtonBuilder().setCustomId(generateUniqueId('generateLeads')).setLabel('Investigate Further').setStyle(ButtonStyle.Primary).setDisabled(true),
        new ButtonBuilder().setCustomId(generateUniqueId('actionLead')).setLabel('Action User').setStyle(ButtonStyle.Danger).setDisabled(true),
        new ButtonBuilder().setCustomId(generateUniqueId('nothing')).setLabel('Close & Exit').setStyle(ButtonStyle.Secondary).setDisabled(true)
    ];
    
    const disabledRow = new ActionRowBuilder().addComponents(buttons);
    
    await interaction.editReply({ components: [disabledRow] }); // Update message to disable buttons

    const action = customId.split('_')[0]; // Extract the action from the custom ID
    switch (action) {
        case 'createCase':
            `<Function Here>`
            break;
        case 'generateLeads':
            `<Function Here>`
            break;
        case 'actionLead':
           `<Function Here>`
                break;
        case 'uploadAvatar':
            `<Function Here>`
            break;
        case 'nothing':
            await interaction.followUp('Closing interaction...');
            break;
    }
}

async function buttonHandler(command, interaction, user1, user2) {
    // Check for active interaction
    if (activeCollectors.has(interaction.user.id)) {
        // Automatically select the "nothing" option for the current interaction
        const currentCollector = activeCollectors.get(interaction.user.id);
        
        // Call the function to handle the "nothing" button interaction
        await handleButtonInteraction(interaction, 'nothing', user1, user2);
        
        // Stop the current interaction
        currentCollector.stop();
    }

    // Define button config for each command
    let buttons;
    if (command === 'null') {
        buttons = [
            new ButtonBuilder().setCustomId(generateUniqueId('errorButton')).setLabel('Error Displaying Button Menu').setStyle(ButtonStyle.Danger).setDisabled(true)
        ];
    } else if (command === 'comparepfp') {
        buttons = [
            new ButtonBuilder().setCustomId(generateUniqueId('createCase')).setLabel('Create Case').setStyle(ButtonStyle.Success),
            new ButtonBuilder().setCustomId(generateUniqueId('generateLeads')).setLabel('Investigate Further').setStyle(ButtonStyle.Primary),
            new ButtonBuilder().setCustomId(generateUniqueId('actionLead')).setLabel('Action User').setStyle(ButtonStyle.Danger),
            new ButtonBuilder().setCustomId(generateUniqueId('nothing')).setLabel('Close & Exit').setStyle(ButtonStyle.Secondary)
        ];
    } else if (command === 'ping') {
        buttons = [
            new ButtonBuilder().setCustomId(generateUniqueId('createPing')).setLabel('Ping').setStyle(ButtonStyle.Success),
            new ButtonBuilder().setCustomId(generateUniqueId('createPong')).setLabel('Pong').setStyle(ButtonStyle.Primary)
        ];
    } else if (command === 'avatar') {
        buttons = [
            new ButtonBuilder().setCustomId(generateUniqueId('uploadAvatar')).setLabel('Attach to A Case').setStyle(ButtonStyle.Success),
            new ButtonBuilder().setCustomId(generateUniqueId('archiveAvatar')).setLabel('Archive Avatar').setStyle(ButtonStyle.Primary),
            new ButtonBuilder().setCustomId(generateUniqueId('nothingAvatar')).setLabel('Close & Exit').setStyle(ButtonStyle.Secondary)
        ];
    } 

    const actionRow = new ActionRowBuilder().addComponents(buttons);
    //const filter = i => i.user.id === interaction.user.id; // Filter to restrict interactions
    const collector = interaction.channel.createMessageComponentCollector({ /*filter,*/ time: parseInt(process.env.SPAM_COOLDOWN, 10) * 1000 || 5000 }); // Collector time

    activeCollectors.set(interaction.user.id, collector); // Store the collector for this user

    collector.on('collect', async i => {
        if (i.user.id !== interaction.user.id) {
            return i.reply({ content: `**<:no:1297791314645090316>\xa0\xa0You cannot use this button because you did not initiate this command!**`, ephemeral: true }); // Deny interaction for unauthorized users
            //return; // Stop processing if the user is not the command initiator
        }

        console.log('Button interaction:', i.customId);
        await i.deferUpdate(); // Defer the interaction update

        // Call the function to handle the button interaction
        await handleButtonInteraction(interaction, i.customId, user1, user2, interaction.client);
        // After handling the interaction, disable the buttons
        const disabledButtons = buttons.map(button => button.setDisabled(true));
        await interaction.editReply({ components: [new ActionRowBuilder().addComponents(disabledButtons)] });
        collector.stop();
    });

    collector.on('end', async () => {
        // Ensure buttons are disabled when collector ends
        const disabledButtons = buttons.map(button => button.setDisabled(true));
        //await interaction.editReply({ components: [disabledRow] });
        await interaction.editReply({ components: [new ActionRowBuilder().addComponents(disabledButtons)] });
        activeCollectors.delete(interaction.user.id); // Clean up the map
    });

    // Return action row to be used in reply
    return actionRow;
}


module.exports = buttonHandler;

Stack traces:

NOTE: This error only happen when multiple users run the commands that provide these buttons at the same time OR when the same user runs the command twice. If the buttons are interacted with before another command is run this error does not occur.

Logged command: avatar by small.fry
Button interaction: nothingAvatar_82c80203-0298-4a80-ba23-82db49d8f2dd
Logged command: avatar by small.fry
Error [InteractionNotReplied]: The reply to this interaction has not been sent or deferred.
    at ChatInputCommandInteraction.editReply (E:\HydraFusion-Bot\node_modules\discord.js\src\structures\interfaces\InteractionResponses.js:161:48)
    at handleButtonInteraction (E:\HydraFusion-Bot\utilities\buttonHandler.js:29:23)
    at buttonHandler (E:\HydraFusion-Bot\utilities\buttonHandler.js:106:15)
    at Object.execute (E:\HydraFusion-Bot\commands\avatar.js:47:33)
    at Client.<anonymous> (E:\HydraFusion-Bot\index.js:88:19)
    at Client.emit (node:events:531:35)
    at InteractionCreateAction.handle (E:\HydraFusion-Bot\node_modules\discord.js\src\client\actions\InteractionCreate.js:97:12)
    at module.exports [as INTERACTION_CREATE] (E:\HydraFusion-Bot\node_modules\discord.js\src\client\websocket\handlers\INTERACTION_CREATE.js:4:36)
    at WebSocketManager.handlePacket (E:\HydraFusion-Bot\node_modules\discord.js\src\client\websocket\WebSocketManager.js:348:31)
    at WebSocketManager.<anonymous> (E:\HydraFusion-Bot\node_modules\discord.js\src\client\websocket\WebSocketManager.js:232:12) {
  code: 'InteractionNotReplied'
}
Logged command: avatar by small.fry
Error [InteractionNotReplied]: The reply to this interaction has not been sent or deferred.
    at ChatInputCommandInteraction.editReply (E:\HydraFusion-Bot\node_modules\discord.js\src\structures\interfaces\InteractionResponses.js:161:48)
    at handleButtonInteraction (E:\HydraFusion-Bot\utilities\buttonHandler.js:29:23)
    at buttonHandler (E:\HydraFusion-Bot\utilities\buttonHandler.js:106:15)
    at Object.execute (E:\HydraFusion-Bot\commands\avatar.js:47:33)
    at Client.<anonymous> (E:\HydraFusion-Bot\index.js:88:19)
    at Client.emit (node:events:531:35)
    at InteractionCreateAction.handle (E:\HydraFusion-Bot\node_modules\discord.js\src\client\actions\InteractionCreate.js:97:12)
    at module.exports [as INTERACTION_CREATE] (E:\HydraFusion-Bot\node_modules\discord.js\src\client\websocket\handlers\INTERACTION_CREATE.js:4:36)
    at WebSocketManager.handlePacket (E:\HydraFusion-Bot\node_modules\discord.js\src\client\websocket\WebSocketManager.js:348:31)
    at WebSocketManager.<anonymous> (E:\HydraFusion-Bot\node_modules\discord.js\src\client\websocket\WebSocketManager.js:232:12) {
  code: 'InteractionNotReplied'
}
Logged command: avatar by small.fry
Button interaction: nothingAvatar_f5f3610d-6657-41fe-b6e7-2e630361c90c
Logged command: avatar by its_weebow
Logged command: avatar by small.fry
Button interaction: archiveAvatar_5370bd72-9bba-4bf1-ba91-f60b81a68db5
E:\HydraFusion-Bot\node_modules\@discordjs\rest\dist\index.js:727
      throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
            ^

DiscordAPIError[10062]: Unknown interaction
    at handleErrors (E:\HydraFusion-Bot\node_modules\@discordjs\rest\dist\index.js:727:13)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async BurstHandler.runRequest (E:\HydraFusion-Bot\node_modules\@discordjs\rest\dist\index.js:831:23)
    at async _REST.request (E:\HydraFusion-Bot\node_modules\@discordjs\rest\dist\index.js:1272:22)
    at async ButtonInteraction.reply (E:\HydraFusion-Bot\node_modules\discord.js\src\structures\interfaces\InteractionResponses.js:115:5) {
  requestBody: {
    files: [],
    json: {
      type: 4,
      data: {
        content: '**<:no:1297791314645090316>  You cannot use this button because you did not initiate this command!**',
        tts: false,
        nonce: undefined,
        enforce_nonce: false,
        embeds: undefined,
        components: undefined,
        username: undefined,
        avatar_url: undefined,
        allowed_mentions: undefined,
        flags: 64,
        message_reference: undefined,
        attachments: undefined,
        sticker_ids: undefined,
        thread_name: undefined,
        applied_tags: undefined,
        poll: undefined
      }
    }
  },
  rawError: { message: 'Unknown interaction', code: 10062 },
  code: 10062,
  status: 404,
  method: 'POST',
  url: 'https://discord.com/api/v10/interactions/1328154629439094835/aW50ZXJhY3Rpb246MTMyODE1NDYyOTQzOTA5NDgzNTpWUGRjS3VORllVandHeWQ0QjhMSE5heTgxOXc5eUt6MGZvZnFNOWlXMFhkbEpmUkhtcERzdHlqS3lhbkdrWTE5dHE3cU92TTFTbkpNMFNWT2VtN0VwVXhNTHBWRThoV0xuZmtUUHg0bjluTm5QalVoaHZlajdiYWtGallKMURPag/callback'
}

Node.js v20.18.0

As you can see above I've already started using crypto to serialize the button custom IDs themselves, which is working fine, but this is not changing the outcome. I need the buttons for each reply to function independently of each-other and specifically to their own interaction but this did not help.

Upvotes: 0

Views: 40

Answers (0)

Related Questions