@@ -12,6 +12,7 @@ const Perms = require('discord.js').Permissions.FLAGS;
1212const commands = require ( './commands' ) ;
1313const config = require ( './config' ) ;
1414const database = require ( './database' ) ;
15+ const UserMutex = require ( './mutex' ) ;
1516const logger = require ( './logger' ) ;
1617const {
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+
2340const 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) {
303341module . exports = {
304342 onGuildJoin,
305343 onGuildLeave,
344+ onGuildMemberUpdate,
306345 onInteraction,
307346 onMessageBulkDelete,
308347 onMessageDelete,
0 commit comments