Collectors

Message collectors

Collectorsopen in new window are useful to enable your bot to obtain *additional* input after the first command was sent. An example would be initiating a quiz, where the bot will "await" a correct response from somebody.

Basic message collector

For now, let's take the example that they have provided us:

// `m` is a message object that will be passed through the filter function
const filter = m => m.content.includes('discord');
const collector = interaction.channel.createMessageCollector({ filter, time: 15000 });

collector.on('collect', m => {
	console.log(`Collected ${m.content}`);
});

collector.on('end', collected => {
	console.log(`Collected ${collected.size} items`);
});
1
2
3
4
5
6
7
8
9
10
11

You can provide a filter key to the object parameter of createMessageCollector(). The value to this key should be a function that returns a boolean value to indicate if this message should be collected or not. To check for multiple conditions in your filter you can connect them using logical operatorsopen in new window. If you don't provide a filter all messages in the channel the collector was started on will be collected.

Note that the above example uses implicit returnopen in new window for the filter function and passes it to the options object using the object property shorthandopen in new window notation.

If a message passes through the filter, it will trigger the collect event for the collector you've created. This message is then passed into the event listener as collected and the provided function is executed. In the above example, you simply log the message. Once the collector finishes collecting based on the provided end conditions the end event emits.

You can control when a collector ends by supplying additional option keys when creating a collector:

  • time: Amount of time in milliseconds the collector should run for
  • max: Number of messages to successfully pass the filter
  • maxProcessed: Number of messages encountered (no matter the filter result)

The benefit of using an event-based collector over .awaitMessages() (its promise-based counterpart) is that you can do something directly after each message is collected, rather than just after the collector ended. You can also stop the collector manually by calling collector.stop().

Await messages

Using TextChannel#awaitMessages()open in new window can be easier if you understand Promises, and it allows you to have cleaner code overall. It is essentially identical to TextChannel#createMessageCollector()open in new window, except promisified. However, the drawback of using this method is that you cannot do things before the Promise is resolved or rejected, either by an error or completion. However, it should do for most purposes, such as awaiting the correct response in a quiz. Instead of taking their example, let's set up a basic quiz command using the .awaitMessages() feature.

First, you'll need some questions and answers to choose from, so here's a basic set:

[
	{
		"question": "What color is the sky?",
		"answers": ["blue"]
	},
	{
		"question": "How many letters are there in the alphabet?",
		"answers": ["26", "twenty-six", "twenty six", "twentysix"]
	}
]
1
2
3
4
5
6
7
8
9
10

The provided set allows for responder error with an array of answers permitted. Ideally, it would be best to place this in a JSON file, which you can call quiz.json for simplicity.

const quiz = require('./quiz.json');
// ...
const item = quiz[Math.floor(Math.random() * quiz.length)];
const filter = response => {
	return item.answers.some(answer => answer.toLowerCase() === response.content.toLowerCase());
};

interaction.reply(item.question, { fetchReply: true })
	.then(() => {
		interaction.channel.awaitMessages({ filter, max: 1, time: 30000, errors: ['time'] })
			.then(collected => {
				interaction.followUp(`${collected.first().author} got the correct answer!`);
			})
			.catch(collected => {
				interaction.followUp('Looks like nobody got the answer this time.');
			});
	});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

TIP

If you don't understand how .some() works, you can read about it in more detail hereopen in new window.

In this filter, you iterate through the answers to find what you want. You would like to ignore the case because simple typos can happen, so you convert each answer to its lowercase form and check if it's equal to the response in lowercase form as well. In the options section, you only want to allow one answer to pass through, hence the max: 1 setting.

The filter looks for messages that match one of the answers in the array of possible answers to pass through the collector. The options (the second parameter) specifies that only a maximum of one message can go through the filter successfully before the Promise successfully resolves. The errors section specifies that time will cause it to error out, which will cause the Promise to reject if one correct answer is not received within the time limit of one minute. As you can see, there is no collect event, so you are limited in that regard.

Reaction collectors

Basic reaction collector

These work quite similarly to message collectors, except that you apply them on a message rather than a channel. This example uses the Message#createReactionCollector()open in new window method. The filter will check for the πŸ‘ emoji–in the default skin tone specifically, so be wary of that. It will also check that the person who reacted shares the same id as the author of the original message that the collector was assigned to.

const filter = (reaction, user) => {
	return reaction.emoji.name === 'πŸ‘' && user.id === message.author.id;
};

const collector = message.createReactionCollector({ filter, time: 15000 });

collector.on('collect', (reaction, user) => {
	console.log(`Collected ${reaction.emoji.name} from ${user.tag}`);
});

collector.on('end', collected => {
	console.log(`Collected ${collected.size} items`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13

Await reactions

Message#awaitReactions()open in new window works almost the same as a reaction collector, except it is Promise-based. The same differences apply as with channel collectors.

const filter = (reaction, user) => {
	return reaction.emoji.name === 'πŸ‘' && user.id === message.author.id;
};

message.awaitReactions({ filter, max: 4, time: 60000, errors: ['time'] })
	.then(collected => console.log(collected.size))
	.catch(collected => {
		console.log(`After a minute, only ${collected.size} out of 4 reacted.`);
	});
1
2
3
4
5
6
7
8
9

Interaction collectors

The third type of collector allows you to collect interactions; such as when users activate a slash command or click on a button in a message.

Basic message component collector

Collecting interactions from message components works similarly to reaction collectors. In the following example, you will check that the interaction came from a button, and that the user clicking the button is the same user that initiated the command.

One important difference to note with interaction collectors is that Discord expects a response to all interactions within 3 seconds - even ones that you don't want to collect. For this reason, you may wish to .deferUpdate() all interactions in your filter, or not use a filter at all and handle this behavior in the collect event.

const collector = message.createMessageComponentCollector({ componentType: 'BUTTON', time: 15000 });

collector.on('collect', i => {
	if (i.user.id === interaction.user.id) {
		i.reply(`${i.user.id} clicked on the ${i.customId} button.`);
	} else {
		i.reply({ content: `These buttons aren't for you!`, ephemeral: true });
	}
});

collector.on('end', collected => {
	console.log(`Collected ${collected.size} interactions.`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13

Await message component

As before, this works similarly to the message component collector, except it is Promise-based.

Unlike other Promise-based collectors, this method will only ever collect one interaction that passes the filter. If no interactions are collected before the time runs out, the Promise will reject. This behavior aligns with Discord's requirement that actions should immediately receive a response. In this example, you will use .deferUpdate() on all interactions in the filter.

const filter = i => {
	i.deferUpdate();
	return i.user.id === interaction.user.id;
};

message.awaitMessageComponent({ filter, componentType: 'SELECT_MENU', time: 60000 })
	.then(interaction => interaction.editReply(`You selected ${interaction.values.join(', ')}!`))
	.catch(err => console.log(`No interactions were collected.`));
1
2
3
4
5
6
7
8