Skip to content

Commit f4a7e32

Browse files
Add role check and role change callbacks
1 parent 7ebe507 commit f4a7e32

1 file changed

Lines changed: 64 additions & 57 deletions

File tree

src/components/role-manager.ts

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import { EarlyReplyMode, TextBasedCommandBuilder } from "../command-abstractions
99
import { TextBasedCommand } from "../command-abstractions/text-based-command.js";
1010
import { skill_roles_order, skill_roles_order_id, Wheatley } from "../wheatley.js";
1111
import { set_interval } from "../utils/node.js";
12-
import { equal } from "../utils/arrays.js";
1312
import { 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+
2627
export 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

Comments
 (0)