@@ -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,21 @@ 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_update_callback = ( member : Discord . GuildMember ) => Promise < void > ;
26+ type role_update_listener = {
27+ roles_of_interest : Set < string > ;
28+ callback : role_update_callback ;
29+ } ;
30+
2631export default class RoleManager extends BotComponent {
2732 private skill_role_log ! : Discord . TextChannel ;
2833 private staff_member_log ! : Discord . TextChannel ;
2934 pink_role ! : Discord . Role ;
3035 interval : NodeJS . Timeout | null = null ;
3136
3237 // 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 ) ;
38+ private roles = new Map < string , Set < string > > ( ) ;
3739
3840 // roles that will not be re-applied on join
3941 blacklisted_roles ! : Set < string > ;
@@ -42,6 +44,17 @@ export default class RoleManager extends BotComponent {
4244 user_roles : user_role_entry ;
4345 } > ( ) ;
4446
47+ private role_checks : role_check [ ] = [ ] ;
48+ private role_update_listeners : role_update_listener [ ] = [ ] ;
49+
50+ register_role_check ( check : role_check ) {
51+ this . role_checks . push ( check ) ;
52+ }
53+
54+ register_role_update_listener ( roles_of_interest : Set < string > , callback : role_update_callback ) {
55+ this . role_update_listeners . push ( { roles_of_interest : roles_of_interest , callback : callback } ) ;
56+ }
57+
4558 override async setup ( commands : CommandSetBuilder ) {
4659 this . skill_role_log = await this . utilities . get_channel ( this . wheatley . channels . skill_role_log ) ;
4760 this . staff_member_log = await this . utilities . get_channel ( this . wheatley . channels . staff_member_log ) ;
@@ -64,6 +77,10 @@ export default class RoleManager extends BotComponent {
6477 }
6578
6679 override async on_ready ( ) {
80+ for await ( const entry of this . database . user_roles . find ( ) ) {
81+ this . roles . set ( entry . user_id , new Set ( entry . roles ) ) ;
82+ }
83+
6784 this . blacklisted_roles = new Set ( [
6885 // general
6986 this . wheatley . roles . root . id ,
@@ -91,35 +108,44 @@ export default class RoleManager extends BotComponent {
91108 this . wheatley . roles . linked_github . id ,
92109 ] ) ;
93110 this . pink_role = unwrap ( await this . wheatley . guild . roles . fetch ( this . wheatley . roles . pink . id ) ) ;
94- this . interval = set_interval ( ( ) => {
111+
112+ this . register_role_check ( this . check_pink . bind ( this ) ) ;
113+ this . register_role_check ( this . check_skill_roles . bind ( this ) ) ;
114+ this . register_role_update_listener ( new Set ( skill_roles_order_id ) , this . check_for_skill_role_bump . bind ( this ) ) ;
115+
116+ const check = ( ) => {
95117 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 ) ) ;
118+ } ;
119+ check ( ) ;
120+ this . interval = set_interval ( check , HOUR ) ;
98121 }
99122
100123 get_highest_skill_role ( roles : string [ ] ) {
101124 return skill_roles_order_id . filter ( id => roles . includes ( id ) ) . at ( - 1 ) ?? null ;
102125 }
103126
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 (
127+ async check_member_roles ( member : Discord . GuildMember ) {
128+ for ( const check of this . role_checks ) {
129+ await check ( member ) ;
130+ }
131+ const old_roles = this . roles . get ( member . id ) ;
132+ const current_roles = member . roles . cache ;
133+ const diff = old_roles ?. symmetricDifference ( current_roles ) ;
134+ if ( diff === undefined || diff . size > 0 ) {
135+ const role_ids = current_roles . map ( role => role . id ) ;
136+ const skill_role = this . get_highest_skill_role ( member . roles . cache . map ( role => role . id ) ) ;
137+ await this . database . user_roles . findOneAndUpdate (
110138 { 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- } ,
139+ { $set : skill_role ? { roles : role_ids , last_known_skill_role : skill_role } : { roles : role_ids } } ,
121140 { upsert : true } ,
122141 ) ;
142+ const new_roles = new Set ( role_ids ) ;
143+ this . roles . set ( member . id , new_roles ) ;
144+ for ( const { roles_of_interest, callback } of this . role_update_listeners ) {
145+ if ( ! roles_of_interest . isDisjointFrom ( diff ?? new_roles ) ) {
146+ await callback ( member ) ;
147+ }
148+ }
123149 }
124150 }
125151
@@ -146,7 +172,7 @@ export default class RoleManager extends BotComponent {
146172 await member . roles . remove ( this . pink_role ) ;
147173 }
148174
149- async handle_pink ( member : Discord . GuildMember ) {
175+ async check_pink ( member : Discord . GuildMember ) {
150176 if ( member . roles . cache . some ( role => role . id == this . wheatley . roles . pink . id ) ) {
151177 if ( member . premiumSince == null ) {
152178 M . log ( "removing pink for" , member . user . tag ) ;
@@ -155,7 +181,7 @@ export default class RoleManager extends BotComponent {
155181 }
156182 }
157183
158- async handle_skill_roles ( member : Discord . GuildMember ) {
184+ async check_skill_roles ( member : Discord . GuildMember ) {
159185 const skill_roles = member . roles . cache . filter ( role =>
160186 Object . values ( this . wheatley . skill_roles ) . some ( skill_role => role . id == skill_role . id ) ,
161187 ) ;
@@ -170,35 +196,26 @@ export default class RoleManager extends BotComponent {
170196 }
171197 }
172198
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 } ) ;
199+ async check_for_skill_role_bump ( member : Discord . GuildMember ) {
200+ const roles_entry = await this . database . user_roles . findOne ( { user_id : member . id } ) ;
178201 const last_known_skill_level =
179202 roles_entry && roles_entry . last_known_skill_role
180203 ? this . wheatley . get_skill_role_index ( roles_entry . last_known_skill_role )
181204 : - 1 ;
182- const current_skill_role = this . get_highest_skill_role ( new_member . roles . cache . map ( role => role . id ) ) ;
205+ const current_skill_role = this . get_highest_skill_role ( member . roles . cache . map ( role => role . id ) ) ;
183206 const current_skill_level = current_skill_role ? this . wheatley . get_skill_role_index ( current_skill_role ) : - 1 ;
184207 if (
185208 current_skill_level > skill_roles_order . indexOf ( "beginner" ) &&
186209 current_skill_level > last_known_skill_level
187210 ) {
188211 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 ) ;
212+ M . log ( "Detected skill level increase for" , member . user . tag ) ;
196213 await this . skill_role_log . send ( {
197214 embeds : [
198215 new Discord . EmbedBuilder ( )
199216 . setAuthor ( {
200- name : new_member . displayName ,
201- iconURL : new_member . displayAvatarURL ( ) ,
217+ name : member . displayName ,
218+ iconURL : member . displayAvatarURL ( ) ,
202219 } )
203220 . setColor ( unwrap ( await this . wheatley . guild . roles . fetch ( current_skill_role ) ) . color )
204221 . setDescription (
@@ -211,12 +228,6 @@ export default class RoleManager extends BotComponent {
211228 }
212229 }
213230
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-
220231 async check_members ( ) {
221232 M . log ( "Starting role checks" ) ;
222233 try {
@@ -230,19 +241,10 @@ export default class RoleManager extends BotComponent {
230241 M . log ( "Finished role checks" ) ;
231242 }
232243
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-
241244 override async on_guild_member_update (
242245 old_member : Discord . GuildMember | Discord . PartialGuildMember ,
243246 new_member : Discord . GuildMember ,
244247 ) {
245- await this . check_for_skill_role_bump ( old_member , new_member ) ;
246248 await this . check_member_roles ( new_member ) ;
247249 }
248250
0 commit comments