@@ -4,64 +4,44 @@ import { colors, HOUR, MINUTE } from "../common.js";
44import { unwrap } from "../utils/misc.js" ;
55import { M } from "../utils/debugging-and-logging.js" ;
66import { BotComponent } from "../bot-component.js" ;
7- import { CommandSetBuilder } from "../command-abstractions/command-set-builder.js" ;
8- import { EarlyReplyMode , TextBasedCommandBuilder } from "../command-abstractions/text-based-command-builder.js" ;
9- import { TextBasedCommand } from "../command-abstractions/text-based-command.js" ;
10- import { skill_roles_order , skill_roles_order_id , Wheatley } from "../wheatley.js" ;
117import { set_interval } from "../utils/node.js" ;
12- import { equal } from "../utils/arrays.js" ;
138import { build_description } from "../utils/strings.js" ;
14- import { SelfClearingSet } from "../utils/containers.js" ;
15-
16- // Role cleanup
17- // Auto-remove pink roles when members are no longer boosting
18- // Auto-remove duplicate skill roles
9+ import { CommandSetBuilder } from "../command-abstractions/command-set-builder.js" ;
1910
20- type user_role_entry = {
11+ export type user_role_entry = {
2112 user_id : string ;
2213 roles : string [ ] ;
23- last_known_skill_role : string | null ;
2414} ;
2515
16+ type role_check = ( member : Discord . GuildMember ) => Promise < void > ;
17+ type role_change_listener = ( role_id : string , member : Discord . GuildMember ) => Promise < void > ;
18+
2619export default class RoleManager extends BotComponent {
27- private skill_role_log ! : Discord . TextChannel ;
2820 private staff_member_log ! : Discord . TextChannel ;
29- pink_role ! : Discord . Role ;
3021 interval : NodeJS . Timeout | null = null ;
3122
3223 // 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 ) ;
24+ private roles = new Map < string , Set < string > > ( ) ;
3725
3826 // roles that will not be re-applied on join
3927 blacklisted_roles ! : Set < string > ;
4028
29+ private role_checks : role_check [ ] = [ ] ;
30+ private on_role_changed = new Map < string , role_change_listener [ ] > ( ) ;
31+
4132 private database = this . wheatley . database . create_proxy < {
4233 user_roles : user_role_entry ;
4334 } > ( ) ;
4435
4536 override async setup ( commands : CommandSetBuilder ) {
46- this . skill_role_log = await this . utilities . get_channel ( this . wheatley . channels . skill_role_log ) ;
4737 this . staff_member_log = await this . utilities . get_channel ( this . wheatley . channels . staff_member_log ) ;
48-
49- commands . add (
50- new TextBasedCommandBuilder ( "gimmepink" , EarlyReplyMode . ephemeral )
51- . set_description ( "Gives pink" )
52- . set_slash ( false )
53- . set_handler ( this . gibpink . bind ( this ) ) ,
54- ) ;
55-
56- commands . add (
57- new TextBasedCommandBuilder ( "unpink" , EarlyReplyMode . ephemeral )
58- . set_description ( "Takes pink" )
59- . set_slash ( false )
60- . set_handler ( this . unpink . bind ( this ) ) ,
61- ) ;
6238 }
6339
6440 override async on_ready ( ) {
41+ for await ( const entry of this . database . user_roles . find ( ) ) {
42+ this . roles . set ( entry . user_id , new Set ( entry . roles ) ) ;
43+ }
44+
6545 this . blacklisted_roles = new Set ( [
6646 // general
6747 this . wheatley . roles . root . id ,
@@ -88,133 +68,53 @@ export default class RoleManager extends BotComponent {
8868 this . wheatley . roles . herald . id ,
8969 this . wheatley . roles . linked_github . id ,
9070 ] ) ;
91- this . pink_role = unwrap ( await this . wheatley . guild . roles . fetch ( this . wheatley . roles . pink . id ) ) ;
92- this . interval = set_interval ( ( ) => {
93- this . check_members ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
94- } , HOUR ) ;
95- this . startup_recovery ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
96- }
9771
98- get_highest_skill_role ( roles : string [ ] ) {
99- return skill_roles_order_id . filter ( id => roles . includes ( id ) ) . at ( - 1 ) ?? null ;
100- }
101-
102- async update_user_roles ( member : Discord . GuildMember ) {
103- const old_roles = this . roles . get ( member . id ) ?? [ ] ;
104- const current_roles = member . roles . cache . map ( role => role . id ) ;
105- if ( ! equal ( old_roles , current_roles ) ) {
106- const skill_role = this . get_highest_skill_role ( current_roles ) ;
107- await this . database . user_roles . updateOne (
108- { user_id : member . id } ,
109- {
110- $set : skill_role
111- ? {
112- roles : current_roles ,
113- last_known_skill_role : this . get_highest_skill_role ( current_roles ) ,
114- }
115- : {
116- roles : current_roles ,
117- } ,
118- } ,
119- { upsert : true } ,
120- ) ;
121- }
122- }
123-
124- async gibpink ( command : TextBasedCommand ) {
125- const member = await command . get_member ( this . wheatley . guild ) ;
126- if ( member . premiumSince == null ) {
127- await command . reply ( "Nice try." , true , true ) ;
128- return ;
129- }
130- if ( member . roles . cache . some ( r => r . id == this . pink_role . id ) ) {
131- await command . reply ( "You are currently pink" , true , true ) ;
132- return ;
133- }
134- await member . roles . add ( this . pink_role ) ;
135- await command . reply ( "You are now pink" , true , true ) ;
72+ const check = ( ) => {
73+ this . check_members ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
74+ } ;
75+ check ( ) ;
76+ this . interval = set_interval ( check , HOUR ) ;
13677 }
13778
138- async unpink ( command : TextBasedCommand ) {
139- const member = await command . get_member ( this . wheatley . guild ) ;
140- if ( ! member . roles . cache . some ( r => r . id == this . pink_role . id ) ) {
141- await command . reply ( "You are not currently pink" , true , true ) ;
142- return ;
143- }
144- await member . roles . remove ( this . pink_role ) ;
79+ register_role_check ( check : role_check ) {
80+ this . role_checks . push ( check ) ;
14581 }
14682
147- async handle_pink ( member : Discord . GuildMember ) {
148- if ( member . roles . cache . some ( role => role . id == this . wheatley . roles . pink . id ) ) {
149- if ( member . premiumSince == null ) {
150- M . log ( "removing pink for" , member . user . tag ) ;
151- await member . roles . remove ( this . pink_role ) ;
152- }
83+ register_role_update_listener ( role_id : string , listener : role_change_listener ) {
84+ if ( ! this . on_role_changed . has ( role_id ) ) {
85+ this . on_role_changed . set ( role_id , [ ] ) ;
15386 }
87+ unwrap ( this . on_role_changed . get ( role_id ) ) . push ( listener ) ;
15488 }
15589
156- async handle_skill_roles ( member : Discord . GuildMember ) {
157- const skill_roles = member . roles . cache . filter ( role =>
158- Object . values ( this . wheatley . skill_roles ) . some ( skill_role => role . id == skill_role . id ) ,
159- ) ;
160- if ( skill_roles . size > 1 ) {
161- M . log ( "removing duplicate skill roles for" , member . user . tag ) ;
162- skill_roles . sort ( ( a , b ) => b . rawPosition - a . rawPosition ) ;
163- M . debug ( skill_roles . map ( x => x . name ) ) ;
164- M . debug ( skill_roles . map ( x => x . name ) . slice ( 1 ) ) ;
165- for ( const role of skill_roles . map ( x => x ) . slice ( 1 ) ) {
166- await member . roles . remove ( role ) ;
167- }
90+ async check_member_roles ( member : Discord . GuildMember ) {
91+ for ( const check of this . role_checks ) {
92+ await check ( member ) ;
16893 }
169- }
170-
171- async check_for_skill_role_bump (
172- old_member : Discord . GuildMember | Discord . PartialGuildMember ,
173- new_member : Discord . GuildMember ,
174- ) {
175- const roles_entry = await this . database . user_roles . findOne ( { user_id : new_member . id } ) ;
176- const last_known_skill_level =
177- roles_entry && roles_entry . last_known_skill_role
178- ? this . wheatley . get_skill_role_index ( roles_entry . last_known_skill_role )
179- : - 1 ;
180- const current_skill_role = this . get_highest_skill_role ( new_member . roles . cache . map ( role => role . id ) ) ;
181- const current_skill_level = current_skill_role ? this . wheatley . get_skill_role_index ( current_skill_role ) : - 1 ;
182- if (
183- current_skill_level > skill_roles_order . indexOf ( "beginner" ) &&
184- current_skill_level > last_known_skill_level
185- ) {
186- assert ( current_skill_role ) ;
187- const debounce_key = `${ new_member . id } ,${ current_skill_role } ` ;
188- // Updates are bumpy...
189- if ( this . debounce_map . has ( debounce_key ) ) {
190- return ;
94+ const old_roles = this . roles . get ( member . id ) ;
95+ const current_roles = member . roles . cache ;
96+ const diff = old_roles ?. symmetricDifference ( current_roles ) ;
97+ if ( diff === undefined || diff . size > 0 ) {
98+ const role_ids = current_roles . map ( role => role . id ) ;
99+ await this . database . user_roles . findOneAndUpdate (
100+ { user_id : member . id } ,
101+ { $set : { roles : role_ids } } ,
102+ { upsert : true } ,
103+ ) ;
104+ const new_roles = new Set ( role_ids ) ;
105+ this . roles . set ( member . id , new_roles ) ;
106+ const events = ( diff ?? new_roles ) . intersection ( this . on_role_changed ) ;
107+ for ( const id of events ) {
108+ const listeners = this . on_role_changed . get ( id ) ;
109+ if ( listeners ) {
110+ for ( const listener of listeners ) {
111+ await listener ( id , member ) ;
112+ }
113+ }
191114 }
192- this . debounce_map . insert ( debounce_key ) ;
193- M . log ( "Detected skill level increase for" , new_member . user . tag ) ;
194- await this . skill_role_log . send ( {
195- embeds : [
196- new Discord . EmbedBuilder ( )
197- . setAuthor ( {
198- name : new_member . displayName ,
199- iconURL : new_member . displayAvatarURL ( ) ,
200- } )
201- . setColor ( unwrap ( await this . wheatley . guild . roles . fetch ( current_skill_role ) ) . color )
202- . setDescription (
203- roles_entry ?. last_known_skill_role
204- ? `<@&${ roles_entry . last_known_skill_role } > -> <@&${ current_skill_role } >`
205- : `<@&${ current_skill_role } >` ,
206- ) ,
207- ] ,
208- } ) ;
209115 }
210116 }
211117
212- async check_member_roles ( member : Discord . GuildMember ) {
213- await this . handle_pink ( member ) ;
214- await this . handle_skill_roles ( member ) ;
215- await this . update_user_roles ( member ) ;
216- }
217-
218118 async check_members ( ) {
219119 M . log ( "Starting role checks" ) ;
220120 try {
@@ -228,19 +128,10 @@ export default class RoleManager extends BotComponent {
228128 M . log ( "Finished role checks" ) ;
229129 }
230130
231- async startup_recovery ( ) {
232- const entries = await this . database . user_roles . find ( ) . toArray ( ) ;
233- for ( const entry of entries ) {
234- this . roles . set ( entry . user_id , entry . roles ) ;
235- }
236- await this . check_members ( ) ;
237- }
238-
239131 override async on_guild_member_update (
240132 old_member : Discord . GuildMember | Discord . PartialGuildMember ,
241133 new_member : Discord . GuildMember ,
242134 ) {
243- await this . check_for_skill_role_bump ( old_member , new_member ) ;
244135 await this . check_member_roles ( new_member ) ;
245136 }
246137
0 commit comments