Skip to content

Commit 083a5cf

Browse files
authored
Merge pull request #66 from Mimickal/mutex-fix
2 parents 9e3df05 + fde48bf commit 083a5cf

8 files changed

Lines changed: 219 additions & 91 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ todo.txt
33
*.log
44
*.swp
55
*.sqlite3
6-
dev-config.json
6+
*-config.json
7+
invite

docs/hosting.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ like https://github.com/nvm-sh/nvm.
88
This guide assumes you're hosting on a Linux distro with `systemd`. The bot will
99
work on other platforms, but you're on your own figuring that out.
1010

11+
## Bot account setup
12+
13+
Your bot account needs the privileged "Server Members Intent" enabled.
14+
It also needs [these permissions](../README.md#permissions).
15+
1116
## Running as a user (in dev-mode)
1217

1318
Quick and easy. Also (mostly) platform-independent!

package-lock.json

Lines changed: 59 additions & 87 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"name": "reactionrolebot",
3-
"version": "2.0.2",
3+
"version": "2.0.3",
44
"description": "I'm a basic, no BS Discord bot that can assign and unassign roles using message reactions. I am completely free and open source, and always will be.",
55
"main": "src/main.js",
66
"dependencies": {
7+
"async-mutex": "^0.3.2",
78
"discord-command-registry": "^1.2.2",
8-
"discord.js": "^13.3.1",
9+
"discord.js": "^13.7.0",
910
"knex": "^0.20.13",
1011
"lodash": "^4.17.20",
1112
"minimist": "^1.2.6",

src/events.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Perms = require('discord.js').Permissions.FLAGS;
1212
const commands = require('./commands');
1313
const config = require('./config');
1414
const database = require('./database');
15+
const UserMutex = require('./mutex');
1516
const logger = require('./logger');
1617
const {
1718
detail,
@@ -20,6 +21,22 @@ const {
2021
unindent,
2122
} = require('./util');
2223

24+
/**
25+
* Allows us to "lock" a user to prevent multiple events from trying to update
26+
* their roles at the same time.
27+
*
28+
* Any event that modifies a user should acquire a lock on that user first.
29+
* The corresponding `GUILD_MEMBER_UPDATE` event will release the lock. If that
30+
* event doesn't fire for some reason, `UserMutex` has a fallback timer to
31+
* release the lock anyway.
32+
*
33+
* Discord API request promises resolve when Discord *acknowledges* them, **not**
34+
* when it *applies* them. Because of this, Discord.js does not update its
35+
* internal user role cache until it receives the corresponding
36+
* `GUILD_MEMBER_UPDATE` event.
37+
*/
38+
const USER_MUTEX = new UserMutex();
39+
2340
const REQUIRED_PERMISSIONS = Object.freeze({
2441
[Perms.ADD_REACTIONS]: 'Add Reactions',
2542
[Perms.MANAGE_MESSAGES]: 'Manage Messages',
@@ -83,6 +100,17 @@ async function onGuildLeave(guild) {
83100
}
84101
}
85102

103+
/**
104+
* Event handler for when a guild member is updated.
105+
* Releases the mutex lock on the user, since this event is fired once a user's
106+
* roles are fully updated.
107+
*
108+
* See docs for: {@link USER_MUTEX}
109+
*/
110+
async function onGuildMemberUpdate(old_member, new_member) {
111+
USER_MUTEX.unlock(new_member);
112+
}
113+
86114
/**
87115
* Event handler for receiving some kind of interaction.
88116
* Logs the interaction and passes it on to the command handler.
@@ -155,12 +183,14 @@ async function onReactionAdd(reaction, react_user) {
155183
const member = await reaction.message.guild.members.fetch(react_user.id);
156184

157185
// Remove mutually exclusive roles from user
186+
// FIXME this database call should optionally take an array
158187
const mutex_roles = lodash.flatMap(
159188
await Promise.all(role_ids.map(role_id => database.getMutexRoles({
160189
guild_id: reaction.message.guild.id,
161190
role_id: role_id,
162191
})))
163192
);
193+
await USER_MUTEX.lock(member); // See USER_MUTEX comment
164194
try {
165195
await member.roles.remove(mutex_roles, 'Role bot removal (mutex)');
166196
await member.roles.add(role_ids, 'Role bot assignment');
@@ -224,8 +254,16 @@ async function onReactionRemove(reaction, react_user) {
224254
return reaction.message.react(emoji);
225255
}
226256

257+
await USER_MUTEX.lock(react_user); // see USER_MUTEX comment
227258
try {
228259
const member = await reaction.message.guild.members.fetch(react_user.id);
260+
261+
// onGuildMemberUpdate won't fire if we don't actually change roles
262+
if (!role_ids.some(role_id => member.roles.cache.has(role_id))) {
263+
USER_MUTEX.unlock(react_user);
264+
return;
265+
}
266+
229267
await member.roles.remove(role_ids, 'Role bot removal');
230268
logger.info(`Removed Roles ${stringify(role_ids)} from ${stringify(react_user)}`);
231269
} catch (err) {
@@ -303,6 +341,7 @@ async function precache(client) {
303341
module.exports = {
304342
onGuildJoin,
305343
onGuildLeave,
344+
onGuildMemberUpdate,
306345
onInteraction,
307346
onMessageBulkDelete,
308347
onMessageDelete,

src/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ const client = new Discord.Client({
2020
intents: [
2121
Discord.Intents.FLAGS.GUILDS,
2222
Discord.Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
23+
Discord.Intents.FLAGS.GUILD_MEMBERS,
2324
Discord.Intents.FLAGS.GUILD_MESSAGES,
2425
Discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
2526
],
2627
partials: [
2728
// https://discordjs.guide/popular-topics/reactions.html#listening-for-reactions-on-old-messages
29+
Discord.Constants.PartialTypes.GUILD_MEMBER,
2830
Discord.Constants.PartialTypes.MESSAGE,
2931
Discord.Constants.PartialTypes.CHANNEL,
3032
Discord.Constants.PartialTypes.REACTION,
@@ -42,6 +44,7 @@ const Events = Discord.Constants.Events;
4244
client.on(Events.CLIENT_READY, events.onReady);
4345
client.on(Events.GUILD_CREATE, events.onGuildJoin);
4446
client.on(Events.GUILD_DELETE, events.onGuildLeave);
47+
client.on(Events.GUILD_MEMBER_UPDATE, events.onGuildMemberUpdate);
4548
client.on(Events.INTERACTION_CREATE, events.onInteraction);
4649
client.on(Events.MESSAGE_BULK_DELETE, events.onMessageBulkDelete);
4750
client.on(Events.MESSAGE_DELETE, events.onMessageDelete);

0 commit comments

Comments
 (0)