Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to edit message according to reaction in Discord.js (create a list and switch page)

I want my Discord bot to sent a message and then edit it when people react (for example creating a list, and clicking on the right or left arrow will edit the messages and show the next/previous part of the list).

Example:
before reaction:
enter image description here

after reaction:
enter image description here

like image 920
JackRed Avatar asked Dec 13 '22 10:12

JackRed


2 Answers

How to handle a message reaction?

There is 3 ways to react to a message reaction:

  1. Using the function awaitReactions (promise based)
  2. Using a ReactionCollector
  3. Using the messageReactionAdd event

The Difference:

messageReactionAdd is an event linked to the Client:

Emitted whenever a reaction is added to a cached message.

while a ReactionCollector and awaitReactions are linked to a specific message, and won't do anything if a reaction is added to another message.

messageReactionAdd will not be fired if a reaction is added to a cached message (old message). There is a guide for listening on old messages in the Discord.js guide, where this warning is given

This section describes how to use some undocumented APIs to add unsupported functionality into discord.js, and as such you should follow anything here with extreme caution. Anything here is subject to change at any time without notice, and may break other functionality in your bot.

awaitReactions is promise based, and it will only return the collection of all added reactions when the promise is solved (after X reactions have been added, after Y seconds, etc). There isn't a specific support to handle every added reaction. You can put your function in the filter function to get every reactions added, but it's a little hack which is not intended. The ReactionCollector, however, has a collect event.

So, what do I use?

You want to edit a message sent by your bot (because you can't edit other's users message). So ReactionCollector or awaitReactions.

If you want to edit a message after a certain condition has been met (X persons has voted, Y reactions has been added, after 15 mins, ...) (e.g: a vote, where you will allow users to vote during 15 mins), you can both use awaitReactions and ReactionCollector.

But if you want to edit the message based on a specific reaction (as in the example, when reacting to an arrow emoji) you'll have to use a ReactionCollector.

If the message is not cached, you can use messageReactionAdd but it will be more complicated because you will basically have to rewrite an emoji collector but for every emojis.

Note: the ReactionCollector and awaitReactions will be deleted if the bot restart, while messageReactionAdd will work as usual (but you'll lost the variable you have declared, so if you has stored the messages you want to listen, they will also disappear).

What to do?

You'll need different things:

  • The list of emojis which will trigger a function (you can choose to react to every emojis)
  • The condition to stop listening to a message reactions (doesn't apply if you want to listen to every messages with messageReactionAdd
  • A function which take a message and edit it
  • A filter function which will return a boolean: true I want to react to this emoji, false I don't want to react. This function will be based on the list of emojis but can also filter the user reacting, or any others conditions you need
  • A logic to edit your message. e.g: for a list, it will be based on the number of result, the current index and the reaction added

Example: a list of users

List of emojis:

const emojiNext = '➡'; // unicode emoji are identified by the emoji itself
const emojiPrevious = '⬅';
const reactionArrow = [emojiPrevious, emojiNext];

Stopping condition

const time = 60000; // time limit: 1 min

Edit function

Here the function is really simple, the message are pre-generated (except the timestamp and the footer).

const first = () => new Discord.MessageEmbed()
      .setAuthor('TOTO', "https://i.imgur.com/ezC66kZ.png")
      .setColor('#AAA')
      .setTitle('First')
      .setDescription('First');

const second = () => new Discord.MessageEmbed()
      .setAuthor('TOTO', "https://i.imgur.com/ezC66kZ.png")
      .setColor('#548')
      .setTitle('Second')
      .setDescription('Second');

const third = () => new Discord.MessageEmbed()
      .setAuthor('TOTO', "https://i.imgur.com/ezC66kZ.png")
      .setColor('#35D')
      .setTitle('Third')
      .setDescription('Third');

const list = [first, second, third];

function getList(i) {
return list[i]().setTimestamp().setFooter(`Page ${i+1}`); // i+1 because we start at 0
}

Filter function

function filter(reaction, user){
  return (!user.bot) && (reactionArrow.includes(reaction.emoji.name)); // check if the emoji is inside the list of emojis, and if the user is not a bot
}

The logic

note that I use list.length here to avoid going in list[list.length] and beyond. If you don't have list hardcoded, you should pass a limit in argument.
You can also make getList return undefined if the index is invalid and instead of using the index for the boolean condition, compare the returned value to undefined.

function onCollect(emoji, message, i, getList) {
  if ((emoji.name === emojiPrevious) && (i > 0)) {
    message.edit(getList(--i));
  } else if ((emoji.name === emojiNext) && (i < list.length-1)) {
    message.edit(getList(++i));
  }
  return i;
}

This is another logic with another getList function which just return list[i] for example, and not setting a timestamp as the one above does, since trying to do .setTimestamp on undefined will raise an error.

  if (emoji.name === emojiPrevious) {
    const embed = getList(i-1);
    if (embed !== undefined) {
      message.edit(embed);
      i--;
    }
  } else if (emoji.name === emojiNext) {
    const embed = getList(i+1);
    if (embed !== undefined) {
      message.edit(embed);
      i++;
    }
  }
  return i;

Build the constructor

The example is the same as asked in the quesion, editing a message with the arrow function.

We're gonna use a collector:

function createCollectorMessage(message, getList) {
  let i = 0;
  const collector = message.createReactionCollector(filter, { time });
  collector.on('collect', r => {
    i = onCollect(r.emoji, message, i, getList);
  });
  collector.on('end', collected => message.clearReactions());
}

It takes the message we want to listen to. You can also give it a list of content // messages // a database // anything necessary.

Sending a message and adding the collector

function sendList(channel, getList){
  channel.send(getList(0))
    .then(msg => msg.react(emojiPrevious))
    .then(msgReaction => msgReaction.message.react(emojiNext))
    .then(msgReaction => createCollectorMessage(msgReaction.message, getList));
}
like image 175
JackRed Avatar answered May 17 '23 07:05

JackRed


Writing this answer on request from OP

Since this is quite a common thing to want to do, I've written a library to help with this exact thing: discord-dynamic-messages Note that discord-dynamic-messages is a typescript only library.

This is how you would solve the problem using a dynamic message.

Defining your pagination message

import { RichEmbed } from 'discord.js';
import { DynamicMessage, OnReaction } from 'discord-dynamic-messages';

const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

export class PaginationMessage extends DynamicMessage {
  constructor(private embeds: Array<() => RichEmbed>, private embedIndex = 0) {
    super();
  }

  @OnReaction(':arrow_left:')
  public previousEmbed() {
    this.embedIndex = clamp(this.embedIndex - 1, 0, this.embeds.length - 1);
  }

  @OnReaction(':arrow_right:')
  public nextEmbed() {
    this.embedIndex = clamp(this.embedIndex + 1, 0, this.embeds.length - 1);
  }

  public render() {
    return this.embeds[this.embedIndex]()
      .setTimestamp()
      .setFooter(`Page ${this.embedIndex + 1}`);
  }
}

Using your defined pagination message

import { Client, RichEmbed } from 'discord.js';
import { PaginationMessage } from '...'

const first = () => new RichEmbed()
  .setAuthor('TOTO', 'https://i.imgur.com/ezC66kZ.png')
  .setColor('#AAA')
  .setTitle('First')
  .setDescription('First');

const second = () => new RichEmbed()
  .setAuthor('TOTO', 'https://i.imgur.com/ezC66kZ.png')
  .setColor('#548')
  .setTitle('Second')
  .setDescription('Second');

const third = () => new RichEmbed()
  .setAuthor('TOTO', 'https://i.imgur.com/ezC66kZ.png')
  .setColor('#35D')
  .setTitle('Third')
  .setDescription('Third');

const pages = [first, second, third];

const client = new Client();
client.on('ready', () => {
  client.on('message', (message) => {
    new PaginationMessage(pages).sendTo(message.channel);
  });
});
client.login(discord_secret);
like image 39
Olian04 Avatar answered May 17 '23 07:05

Olian04