@@ -9,9 +9,7 @@ import { EarlyReplyMode, TextBasedCommandBuilder } from "../command-abstractions
99import { TextBasedCommand } from "../command-abstractions/text-based-command.js" ;
1010import { skill_roles_order , skill_roles_order_id , Wheatley } from "../wheatley.js" ;
1111import { set_interval } from "../utils/node.js" ;
12- import { equal } from "../utils/arrays.js" ;
1312import { build_description } from "../utils/strings.js" ;
14- import { SelfClearingSet } from "../utils/containers.js" ;
1513
1614// Role cleanup
1715// Auto-remove pink roles when members are no longer boosting
@@ -23,17 +21,17 @@ type user_role_entry = {
2321 last_known_skill_role : string | null ;
2422} ;
2523
24+ type role_check = ( member : Discord . GuildMember ) => Promise < void > ;
25+ type role_change_listener = ( role_id : string , member : Discord . GuildMember ) => Promise < void > ;
26+
2627export default class RoleManager extends BotComponent {
2728 private skill_role_log ! : Discord . TextChannel ;
2829 private staff_member_log ! : Discord . TextChannel ;
2930 pink_role ! : Discord . Role ;
3031 interval : NodeJS . Timeout | null = null ;
3132
3233 // current database state
33- roles = new Map < string , string [ ] > ( ) ;
34-
35- // stores user id + role id encoded as a user_id,role_id string
36- debounce_map = new SelfClearingSet ( MINUTE ) ;
34+ private roles = new Map < string , Set < string > > ( ) ;
3735
3836 // roles that will not be re-applied on join
3937 blacklisted_roles ! : Set < string > ;
@@ -42,6 +40,20 @@ export default class RoleManager extends BotComponent {
4240 user_roles : user_role_entry ;
4341 } > ( ) ;
4442
43+ private role_checks : role_check [ ] = [ ] ;
44+ private on_role_changed = new Map < string , role_change_listener [ ] > ( ) ;
45+
46+ register_role_check ( check : role_check ) {
47+ this . role_checks . push ( check ) ;
48+ }
49+
50+ register_role_update_listener ( role_id : string , listener : role_change_listener ) {
51+ if ( ! this . on_role_changed . has ( role_id ) ) {
52+ this . on_role_changed . set ( role_id , [ ] ) ;
53+ }
54+ unwrap ( this . on_role_changed . get ( role_id ) ) . push ( listener ) ;
55+ }
56+
4557 override async setup ( commands : CommandSetBuilder ) {
4658 this . skill_role_log = await this . utilities . get_channel ( this . wheatley . channels . skill_role_log ) ;
4759 this . staff_member_log = await this . utilities . get_channel ( this . wheatley . channels . staff_member_log ) ;
@@ -64,6 +76,10 @@ export default class RoleManager extends BotComponent {
6476 }
6577
6678 override async on_ready ( ) {
79+ for await ( const entry of this . database . user_roles . find ( ) ) {
80+ this . roles . set ( entry . user_id , new Set ( entry . roles ) ) ;
81+ }
82+
6783 this . blacklisted_roles = new Set ( [
6884 // general
6985 this . wheatley . roles . root . id ,
@@ -91,35 +107,50 @@ export default class RoleManager extends BotComponent {
91107 this . wheatley . roles . linked_github . id ,
92108 ] ) ;
93109 this . pink_role = unwrap ( await this . wheatley . guild . roles . fetch ( this . wheatley . roles . pink . id ) ) ;
94- this . interval = set_interval ( ( ) => {
110+
111+ this . register_role_check ( this . check_pink . bind ( this ) ) ;
112+ this . register_role_check ( this . check_skill_roles . bind ( this ) ) ;
113+ for ( const id of skill_roles_order_id ) {
114+ this . register_role_update_listener ( id , this . check_for_skill_role_bump . bind ( this ) ) ;
115+ }
116+
117+ const check = ( ) => {
95118 this . check_members ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
96- } , HOUR ) ;
97- this . startup_recovery ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
119+ } ;
120+ check ( ) ;
121+ this . interval = set_interval ( check , HOUR ) ;
98122 }
99123
100124 get_highest_skill_role ( roles : string [ ] ) {
101125 return skill_roles_order_id . filter ( id => roles . includes ( id ) ) . at ( - 1 ) ?? null ;
102126 }
103127
104- async update_user_roles ( member : Discord . GuildMember ) {
105- const old_roles = this . roles . get ( member . id ) ?? [ ] ;
106- const current_roles = member . roles . cache . map ( role => role . id ) ;
107- if ( ! equal ( old_roles , current_roles ) ) {
108- const skill_role = this . get_highest_skill_role ( current_roles ) ;
109- await this . database . user_roles . updateOne (
128+ async check_member_roles ( member : Discord . GuildMember ) {
129+ for ( const check of this . role_checks ) {
130+ await check ( member ) ;
131+ }
132+ const old_roles = this . roles . get ( member . id ) ;
133+ const current_roles = member . roles . cache ;
134+ const diff = old_roles ?. symmetricDifference ( current_roles ) ;
135+ if ( diff === undefined || diff . size > 0 ) {
136+ const role_ids = current_roles . map ( role => role . id ) ;
137+ const skill_role = this . get_highest_skill_role ( member . roles . cache . map ( role => role . id ) ) ;
138+ await this . database . user_roles . findOneAndUpdate (
110139 { user_id : member . id } ,
111- {
112- $set : skill_role
113- ? {
114- roles : current_roles ,
115- last_known_skill_role : this . get_highest_skill_role ( current_roles ) ,
116- }
117- : {
118- roles : current_roles ,
119- } ,
120- } ,
140+ { $set : skill_role ? { roles : role_ids } : { roles : role_ids , last_known_skill_role : skill_role } } ,
121141 { upsert : true } ,
122142 ) ;
143+ const new_roles = new Set ( role_ids ) ;
144+ this . roles . set ( member . id , new_roles ) ;
145+ const events = ( diff ?? new_roles ) . intersection ( this . on_role_changed ) ;
146+ for ( const id of events ) {
147+ const listeners = this . on_role_changed . get ( id ) ;
148+ if ( listeners ) {
149+ for ( const listener of listeners ) {
150+ await listener ( id , member ) ;
151+ }
152+ }
153+ }
123154 }
124155 }
125156
@@ -146,7 +177,7 @@ export default class RoleManager extends BotComponent {
146177 await member . roles . remove ( this . pink_role ) ;
147178 }
148179
149- async handle_pink ( member : Discord . GuildMember ) {
180+ async check_pink ( member : Discord . GuildMember ) {
150181 if ( member . roles . cache . some ( role => role . id == this . wheatley . roles . pink . id ) ) {
151182 if ( member . premiumSince == null ) {
152183 M . log ( "removing pink for" , member . user . tag ) ;
@@ -155,7 +186,7 @@ export default class RoleManager extends BotComponent {
155186 }
156187 }
157188
158- async handle_skill_roles ( member : Discord . GuildMember ) {
189+ async check_skill_roles ( member : Discord . GuildMember ) {
159190 const skill_roles = member . roles . cache . filter ( role =>
160191 Object . values ( this . wheatley . skill_roles ) . some ( skill_role => role . id == skill_role . id ) ,
161192 ) ;
@@ -170,35 +201,26 @@ export default class RoleManager extends BotComponent {
170201 }
171202 }
172203
173- async check_for_skill_role_bump (
174- old_member : Discord . GuildMember | Discord . PartialGuildMember ,
175- new_member : Discord . GuildMember ,
176- ) {
177- const roles_entry = await this . database . user_roles . findOne ( { user_id : new_member . id } ) ;
204+ async check_for_skill_role_bump ( role_id : string , member : Discord . GuildMember ) {
205+ const roles_entry = await this . database . user_roles . findOne ( { user_id : member . id } ) ;
178206 const last_known_skill_level =
179207 roles_entry && roles_entry . last_known_skill_role
180208 ? this . wheatley . get_skill_role_index ( roles_entry . last_known_skill_role )
181209 : - 1 ;
182- const current_skill_role = this . get_highest_skill_role ( new_member . roles . cache . map ( role => role . id ) ) ;
210+ const current_skill_role = this . get_highest_skill_role ( member . roles . cache . map ( role => role . id ) ) ;
183211 const current_skill_level = current_skill_role ? this . wheatley . get_skill_role_index ( current_skill_role ) : - 1 ;
184212 if (
185213 current_skill_level > skill_roles_order . indexOf ( "beginner" ) &&
186214 current_skill_level > last_known_skill_level
187215 ) {
188216 assert ( current_skill_role ) ;
189- const debounce_key = `${ new_member . id } ,${ current_skill_role } ` ;
190- // Updates are bumpy...
191- if ( this . debounce_map . has ( debounce_key ) ) {
192- return ;
193- }
194- this . debounce_map . insert ( debounce_key ) ;
195- M . log ( "Detected skill level increase for" , new_member . user . tag ) ;
217+ M . log ( "Detected skill level increase for" , member . user . tag ) ;
196218 await this . skill_role_log . send ( {
197219 embeds : [
198220 new Discord . EmbedBuilder ( )
199221 . setAuthor ( {
200- name : new_member . displayName ,
201- iconURL : new_member . displayAvatarURL ( ) ,
222+ name : member . displayName ,
223+ iconURL : member . displayAvatarURL ( ) ,
202224 } )
203225 . setColor ( unwrap ( await this . wheatley . guild . roles . fetch ( current_skill_role ) ) . color )
204226 . setDescription (
@@ -211,12 +233,6 @@ export default class RoleManager extends BotComponent {
211233 }
212234 }
213235
214- async check_member_roles ( member : Discord . GuildMember ) {
215- await this . handle_pink ( member ) ;
216- await this . handle_skill_roles ( member ) ;
217- await this . update_user_roles ( member ) ;
218- }
219-
220236 async check_members ( ) {
221237 M . log ( "Starting role checks" ) ;
222238 try {
@@ -230,19 +246,10 @@ export default class RoleManager extends BotComponent {
230246 M . log ( "Finished role checks" ) ;
231247 }
232248
233- async startup_recovery ( ) {
234- const entries = await this . database . user_roles . find ( ) . toArray ( ) ;
235- for ( const entry of entries ) {
236- this . roles . set ( entry . user_id , entry . roles ) ;
237- }
238- await this . check_members ( ) ;
239- }
240-
241249 override async on_guild_member_update (
242250 old_member : Discord . GuildMember | Discord . PartialGuildMember ,
243251 new_member : Discord . GuildMember ,
244252 ) {
245- await this . check_for_skill_role_bump ( old_member , new_member ) ;
246253 await this . check_member_roles ( new_member ) ;
247254 }
248255
0 commit comments