Search code examples
node.jstypescriptdiscorddiscord.js

How to update multiple time a discord.js's interaction using setTimeout?


I'm currently working on a discordjs v13.6 bot in typescript who post an embed object with the date of day when using /day interaction command. In a nutshell: I make /day and an embed is posted by my bot with the current date, weather, etc...

It's work fine and I added 3 buttons under the embed:

  1. "reload" button: it will simply update the embed (with current weather forecast).
  2. "previous" button: to update embed's interaction to the previous day.
  3. "next" button: to update embed's interaction to the next day.

My code work as it should like this for the 3 buttons attached to my embed:

import { MessageActionRow, MessageButton } from 'discord.js';

// All customId property are formated like: `{method}@{date}`, ie: `reload@2022-03-10` is the button who'll reload the embed to date 03/10/2022.
export const createNavigationButtons = (date) => (
  new MessageActionRow().addComponents([
    new MessageButton()
      .setCustomId(`prev@${date}`)
      .setStyle('SECONDARY')
      .setEmoji("946186554517389332"),
    new MessageButton()
      .setCustomId(`reload@${date}`)
      .setStyle('SUCCESS')
      .setEmoji("946190012154794055"),
    new MessageButton()
      .setCustomId(`next@${date}`)
      .setStyle('SECONDARY')
      .setEmoji("946186699745161296")
  ])
)

For the logic:

import { ButtonInteraction               } from 'discord.js';
import { createEmbed                     } from "../utils/embed";
import { createNavigationButtons         } from "../utils/buttons";
import * as year                           from "../../resources/year.json";
import * as moment                         from 'moment';
import { setTimeout as wait }  from 'node:timers/promises';

// This function is called in the on('interactionCreate') event, when interaction.isButton() is true
export const button = async (interaction: ButtonInteraction): Promise<void> => {
  const [action, date]: string[] = interaction.customId?.split('@');
  await interaction.deferUpdate();
  await wait(1000);
  const newDate: string = {
    prev:   moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
    next:   moment(date, "YYYY-MM-DD").add(     1, "day").format("YYYY-MM-DD"),
    reload: date
  }[action];
  await interaction.editReply({
    embeds:     [await createEmbed(year[newDate])],
    components: [createNavigationButtons(newDate)]
  });
}

It works just as I wished. BUT, everybody can use theses buttons (and I don't want to send /day's answer as ephemeral, I want everybody to see the response). So, if we use /day 2022-03-10 the embed for March 10, 2022. but if the author or someone else (I don't mind) use the button, the embed will be updated with another date (and that's fine by me !). But I want to roll back my embed to the original date few seconds / minutes after the button is pressed.

I tried somes primitive way like the setTimeout like this:

export const button = async (interaction: ButtonInteraction, config: any): Promise<void> => {
  // In this test, button's customId are formated like {method}@{date}@{date's origin} (where 'origin' is the original requested's date, optional)
  const [action, date, origin]: string[] = interaction.customId?.split('@');
  await interaction.deferUpdate();
  await wait(1000);
  const newDate: string = {
    prev:   moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
    next:   moment(date, "YYYY-MM-DD").add(     1, "day").format("YYYY-MM-DD"),
    reload: date
  }[action];
  // Here is my setTimeout who is supposed to recursively recall this function with a "reload" and the original date
  setTimeout(async () => {
    interaction.customId = `reload@${origin ?? date}`;
    console.log(interaction.customId);
    await button(interaction, config);
  }, 5000);
  await interaction.editReply({
    embeds:     [await createEmbed(year[newDate])],
    components: [createNavigationButtons(newDate)]
  });
};

When I press my buttons with this, it's correctly updated but 5sec (setTimeout's value) after it end up with an error saying:

reload@2022-03-10
/home/toto/tata/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:180
    if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
                                             ^
Error [INTERACTION_ALREADY_REPLIED]: The reply to this interaction has already been sent or deferred.
    at ButtonInteraction.deferUpdate (/home/toto/tata/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:180:46)
    at button (/home/toto/tata/src/services/button.ts:12:21)
    at Timeout._onTimeout (/home/toto/tata/src/services/button.ts:28:21)
    at listOnTimeout (node:internal/timers:559:17)
    at processTimers (node:internal/timers:502:7) {
  [Symbol(code)]: 'INTERACTION_ALREADY_REPLIED'
}

I understand that it seems I can't reupdate my interaction with the same token like that, so how shoul I achieve my goal ? may be the setTimeout isn't a propper solution (but it was quite simple to implemant so I tried it first). Any Ideas ?


Solution

  • I successfully reached my objective like this:

    // All customId property are formated like: `{method}@{date}@{origin}`, ie: `reload@2022-03-10` is the button who'll reload the embed to date 03/10/2022.
    export const createNavigationButtons = (date: string, mode?: boolean) => (
      new MessageActionRow().addComponents([
        new MessageButton()
          .setCustomId(`prev@${date}${!mode ? `@${date}` : ''}`)
          .setStyle('SECONDARY')
          .setEmoji("946186554517389332"),
        new MessageButton()
          .setCustomId(`reload@${date}`)
          .setStyle('SUCCESS')
          .setEmoji("946190012154794055"),
        new MessageButton()
          .setCustomId(`remind@${date}`)
          .setStyle('PRIMARY')
          .setEmoji("946192601806155806"),
        new MessageButton()
          .setCustomId(`next@${date}${!mode ? `@${date}` : ''}`)
          .setStyle('SECONDARY')
          .setEmoji("946186699745161296")
      ])
    );
    
    export const createButtons = (date, mode?: boolean) => ({
      components: [ createNavigationButtons(date, mode) ]
    });
    
    export const button = async (interaction: ButtonInteraction, config: any): Promise<void> => {
      const [action, date, origin]: string[] = interaction.customId?.split('@');
      const newDate: string = {
        prev:   moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
        next:   moment(date, "YYYY-MM-DD").add(     1, "day").format("YYYY-MM-DD"),
        reload: date
      }[action];
      origin && !interaction.deferred && setTimeout(async () => {
        await interaction.editReply({
          embeds: [await createEmbed(year[origin], config.server)],
          ...createButtons(origin)
        });
      }, 120000);
      !interaction.deferred && await interaction.deferUpdate();
      await interaction.editReply({
        embeds: [await createEmbed(year[newDate], config.server)],
        ...createButtons(newDate, true)
      });
    };
    

    In a nutshell, when I first create the embed I place a @{origin} (who's a date), and when I navigate and update my embed with my buttons, I don't send origin (only {action}@{date}, not {action}@{date}@origin by passing true to my createButtons method.