@@ -4,59 +4,38 @@ 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" ;
159
16- // Role cleanup
17- // Auto-remove pink roles when members are no longer boosting
18- // Auto-remove duplicate skill roles
19-
20- type user_role_entry = {
10+ export type user_role_entry = {
2111 user_id : string ;
2212 roles : string [ ] ;
23- last_known_skill_role : string | null ;
2413} ;
2514
15+ type role_check = ( member : Discord . GuildMember ) => Promise < void > ;
16+ type role_change_listener = ( role_id : string , member : Discord . GuildMember ) => Promise < void > ;
17+
2618export default class RoleManager extends BotComponent {
27- pink_role : Discord . Role ;
28- interval : NodeJS . Timeout | null = null ;
19+ private interval : NodeJS . Timeout | null = null ;
2920
3021 // current database state
31- roles = new Map < string , string [ ] > ( ) ;
32-
33- // stores user id + role id encoded as a user_id,role_id string
34- debounce_map = new SelfClearingSet ( MINUTE ) ;
22+ private roles = new Map < string , Set < string > > ( ) ;
3523
3624 // roles that will not be re-applied on join
37- blacklisted_roles : Set < string > ;
25+ private blacklisted_roles : Set < string > ;
26+
27+ private role_checks : role_check [ ] = [ ] ;
28+ private on_role_changed = new Map < string , role_change_listener [ ] > ( ) ;
3829
3930 private database = unwrap ( this . wheatley . database ) . create_proxy < {
4031 user_roles : user_role_entry ;
4132 } > ( ) ;
4233
43- override async setup ( commands : CommandSetBuilder ) {
44- commands . add (
45- new TextBasedCommandBuilder ( "gimmepink" , EarlyReplyMode . ephemeral )
46- . set_description ( "Gives pink" )
47- . set_slash ( false )
48- . set_handler ( this . gibpink . bind ( this ) ) ,
49- ) ;
50-
51- commands . add (
52- new TextBasedCommandBuilder ( "unpink" , EarlyReplyMode . ephemeral )
53- . set_description ( "Takes pink" )
54- . set_slash ( false )
55- . set_handler ( this . unpink . bind ( this ) ) ,
56- ) ;
57- }
58-
5934 override async on_ready ( ) {
35+ for await ( const entry of this . database . user_roles . find ( ) ) {
36+ this . roles . set ( entry . user_id , new Set ( entry . roles ) ) ;
37+ }
38+
6039 this . blacklisted_roles = new Set ( [
6140 // general
6241 this . wheatley . roles . root . id ,
@@ -83,132 +62,53 @@ export default class RoleManager extends BotComponent {
8362 this . wheatley . roles . herald . id ,
8463 this . wheatley . roles . linked_github . id ,
8564 ] ) ;
86- this . pink_role = unwrap ( await this . wheatley . TCCPP . roles . fetch ( this . wheatley . roles . pink . id ) ) ;
87- this . interval = set_interval ( ( ) => {
88- this . check_members ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
89- } , HOUR ) ;
90- this . startup_recovery ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
91- }
9265
93- get_highest_skill_role ( roles : string [ ] ) {
94- return skill_roles_order_id . filter ( id => roles . includes ( id ) ) . at ( - 1 ) ?? null ;
95- }
96-
97- async update_user_roles ( member : Discord . GuildMember ) {
98- const old_roles = this . roles . get ( member . id ) ?? [ ] ;
99- const current_roles = member . roles . cache . map ( role => role . id ) ;
100- if ( ! equal ( old_roles , current_roles ) ) {
101- const skill_role = this . get_highest_skill_role ( current_roles ) ;
102- await this . database . user_roles . updateOne (
103- { user_id : member . id } ,
104- {
105- $set : skill_role
106- ? {
107- roles : current_roles ,
108- last_known_skill_role : this . get_highest_skill_role ( current_roles ) ,
109- }
110- : {
111- roles : current_roles ,
112- } ,
113- } ,
114- { upsert : true } ,
115- ) ;
116- }
117- }
118-
119- async gibpink ( command : TextBasedCommand ) {
120- const member = await command . get_member ( this . wheatley . TCCPP ) ;
121- if ( member . premiumSince == null ) {
122- await command . reply ( "Nice try." , true , true ) ;
123- return ;
124- }
125- if ( member . roles . cache . some ( r => r . id == this . pink_role . id ) ) {
126- await command . reply ( "You are currently pink" , true , true ) ;
127- return ;
128- }
129- await member . roles . add ( this . pink_role ) ;
66+ const check = ( ) => {
67+ this . check_members ( ) . catch ( this . wheatley . critical_error . bind ( this . wheatley ) ) ;
68+ } ;
69+ check ( ) ;
70+ this . interval = set_interval ( check , HOUR ) ;
13071 }
13172
132- async unpink ( command : TextBasedCommand ) {
133- const member = await command . get_member ( this . wheatley . TCCPP ) ;
134- if ( ! member . roles . cache . some ( r => r . id == this . pink_role . id ) ) {
135- await command . reply ( "You are not currently pink" , true , true ) ;
136- return ;
137- }
138- await member . roles . remove ( this . pink_role ) ;
73+ register_role_check ( check : role_check ) {
74+ this . role_checks . push ( check ) ;
13975 }
14076
141- async handle_pink ( member : Discord . GuildMember ) {
142- if ( member . roles . cache . some ( role => role . id == this . wheatley . roles . pink . id ) ) {
143- if ( member . premiumSince == null ) {
144- M . log ( "removing pink for" , member . user . tag ) ;
145- await member . roles . remove ( this . pink_role ) ;
146- }
77+ register_role_update_listener ( role_id : string , listener : role_change_listener ) {
78+ if ( ! this . on_role_changed . has ( role_id ) ) {
79+ this . on_role_changed . set ( role_id , [ ] ) ;
14780 }
81+ unwrap ( this . on_role_changed . get ( role_id ) ) . push ( listener ) ;
14882 }
14983
150- async handle_skill_roles ( member : Discord . GuildMember ) {
151- const skill_roles = member . roles . cache . filter ( role =>
152- Object . values ( this . wheatley . skill_roles ) . some ( skill_role => role . id == skill_role . id ) ,
153- ) ;
154- if ( skill_roles . size > 1 ) {
155- M . log ( "removing duplicate skill roles for" , member . user . tag ) ;
156- skill_roles . sort ( ( a , b ) => b . rawPosition - a . rawPosition ) ;
157- M . debug ( skill_roles . map ( x => x . name ) ) ;
158- M . debug ( skill_roles . map ( x => x . name ) . slice ( 1 ) ) ;
159- for ( const role of skill_roles . map ( x => x ) . slice ( 1 ) ) {
160- await member . roles . remove ( role ) ;
161- }
84+ async check_member_roles ( member : Discord . GuildMember ) {
85+ for ( const check of this . role_checks ) {
86+ await check ( member ) ;
16287 }
163- }
164-
165- async check_for_skill_role_bump (
166- old_member : Discord . GuildMember | Discord . PartialGuildMember ,
167- new_member : Discord . GuildMember ,
168- ) {
169- const roles_entry = await this . database . user_roles . findOne ( { user_id : new_member . id } ) ;
170- const last_known_skill_level =
171- roles_entry && roles_entry . last_known_skill_role
172- ? this . wheatley . get_skill_role_index ( roles_entry . last_known_skill_role )
173- : - 1 ;
174- const current_skill_role = this . get_highest_skill_role ( new_member . roles . cache . map ( role => role . id ) ) ;
175- const current_skill_level = current_skill_role ? this . wheatley . get_skill_role_index ( current_skill_role ) : - 1 ;
176- if (
177- current_skill_level > skill_roles_order . indexOf ( "beginner" ) &&
178- current_skill_level > last_known_skill_level
179- ) {
180- assert ( current_skill_role ) ;
181- const debounce_key = `${ new_member . id } ,${ current_skill_role } ` ;
182- // Updates are bumpy...
183- if ( this . debounce_map . has ( debounce_key ) ) {
184- return ;
88+ const old_roles = this . roles . get ( member . id ) ;
89+ const current_roles = member . roles . cache ;
90+ const diff = old_roles ?. symmetricDifference ( current_roles ) ;
91+ if ( diff === undefined || diff . size > 0 ) {
92+ const role_ids = current_roles . map ( role => role . id ) ;
93+ await this . database . user_roles . findOneAndUpdate (
94+ { user_id : member . id } ,
95+ { $set : { roles : role_ids } } ,
96+ { upsert : true } ,
97+ ) ;
98+ const new_roles = new Set ( role_ids ) ;
99+ this . roles . set ( member . id , new_roles ) ;
100+ const events = ( diff ?? new_roles ) . intersection ( this . on_role_changed ) ;
101+ for ( const id of events ) {
102+ const listeners = this . on_role_changed . get ( id ) ;
103+ if ( listeners ) {
104+ for ( const listener of listeners ) {
105+ await listener ( id , member ) ;
106+ }
107+ }
185108 }
186- this . debounce_map . insert ( debounce_key ) ;
187- M . log ( "Detected skill level increase for" , new_member . user . tag ) ;
188- await this . wheatley . channels . skill_role_log . send ( {
189- embeds : [
190- new Discord . EmbedBuilder ( )
191- . setAuthor ( {
192- name : new_member . displayName ,
193- iconURL : new_member . displayAvatarURL ( ) ,
194- } )
195- . setColor ( unwrap ( await this . wheatley . TCCPP . roles . fetch ( current_skill_role ) ) . color )
196- . setDescription (
197- roles_entry ?. last_known_skill_role
198- ? `<@&${ roles_entry . last_known_skill_role } > -> <@&${ current_skill_role } >`
199- : `<@&${ current_skill_role } >` ,
200- ) ,
201- ] ,
202- } ) ;
203109 }
204110 }
205111
206- async check_member_roles ( member : Discord . GuildMember ) {
207- await this . handle_pink ( member ) ;
208- await this . handle_skill_roles ( member ) ;
209- await this . update_user_roles ( member ) ;
210- }
211-
212112 async check_members ( ) {
213113 M . log ( "Starting role checks" ) ;
214114 try {
@@ -222,19 +122,10 @@ export default class RoleManager extends BotComponent {
222122 M . log ( "Finished role checks" ) ;
223123 }
224124
225- async startup_recovery ( ) {
226- const entries = await this . database . user_roles . find ( ) . toArray ( ) ;
227- for ( const entry of entries ) {
228- this . roles . set ( entry . user_id , entry . roles ) ;
229- }
230- await this . check_members ( ) ;
231- }
232-
233125 override async on_guild_member_update (
234126 old_member : Discord . GuildMember | Discord . PartialGuildMember ,
235127 new_member : Discord . GuildMember ,
236128 ) {
237- await this . check_for_skill_role_bump ( old_member , new_member ) ;
238129 await this . check_member_roles ( new_member ) ;
239130 }
240131
0 commit comments