Skip to content

Commit 1c8e874

Browse files
Refactor skill role stuff
1 parent 554a4ea commit 1c8e874

9 files changed

Lines changed: 252 additions & 256 deletions

File tree

src/components/anti-invite-links.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,12 @@ export default class AntiInviteLinks extends BotComponent {
3333
this.staff_flag_log = await this.utilities.get_channel(this.wheatley.channels.staff_flag_log);
3434
}
3535

36-
async member_is_proficient_or_higher(member: Discord.GuildMember | null) {
37-
if (!member) {
38-
return false;
39-
}
40-
const skill_roles = member.roles.cache.filter(role =>
41-
Object.values(this.wheatley.skill_roles).some(skill_role => role.id == skill_role.id),
42-
);
43-
if (skill_roles.size > 1) {
44-
const skill_role_ranks = Object.values(this.wheatley.skill_roles).map(role => role.id);
45-
const proficient_index = skill_role_ranks.indexOf(this.wheatley.skill_roles.proficient.id);
46-
assert(proficient_index !== -1);
47-
return skill_roles.some(role => skill_role_ranks.indexOf(role.id) >= proficient_index);
48-
}
49-
return false;
50-
}
51-
5236
async handle_message(message: Discord.Message) {
5337
if (await this.wheatley.check_permissions(message.author, Discord.PermissionFlagsBits.ModerateMembers)) {
5438
return;
5539
}
5640
const match = match_invite(message.content);
57-
if (match && !whitelist.includes(match) && !(await this.member_is_proficient_or_higher(message.member))) {
41+
if (match && !whitelist.includes(match)) {
5842
const quote = await this.utilities.make_quote_embeds([message]);
5943
await message.delete();
6044
assert(!(message.channel instanceof Discord.PartialGroupDMChannel));

src/components/moderation/modmail.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,8 @@ export default class Modmail extends BotComponent {
278278
});
279279
await this.log_action(interaction.member, "Modmail button spammed");
280280
} else {
281-
const member = await this.wheatley.guild.members.fetch(interaction.user.id);
282-
const non_beginner_skill_roles = member.roles.cache.filter(role =>
283-
Object.values(this.wheatley.skill_roles).some(
284-
skill_role => role.id == skill_role.id && skill_role.name != "Beginner",
285-
),
286-
);
287-
if (non_beginner_skill_roles.size > 0) {
288-
// fast-path people who can read
281+
if (await this.wheatley.is_established_member(interaction.user)) {
282+
// fast-path established members
289283
await interaction.deferReply({
290284
ephemeral: true,
291285
});

src/components/role-manager.ts

Lines changed: 47 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -4,64 +4,44 @@ import { colors, HOUR, MINUTE } from "../common.js";
44
import { unwrap } from "../utils/misc.js";
55
import { M } from "../utils/debugging-and-logging.js";
66
import { 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";
117
import { set_interval } from "../utils/node.js";
12-
import { equal } from "../utils/arrays.js";
138
import { 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+
2619
export 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

src/modules/tccpp/components/anti-screenshot.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,8 @@ export default class AntiScreenshot extends BotComponent {
103103
async anti_screenshot(starter_message: Discord.Message, thread: Discord.ThreadChannel) {
104104
await delay(1000);
105105
assert(starter_message);
106-
assert(starter_message.member);
107-
// trust people with skill roles
108-
if (this.wheatley.has_skill_roles_other_than_beginner(starter_message.member)) {
106+
// trust established members
107+
if (await this.wheatley.is_established_member(starter_message.author)) {
109108
return;
110109
}
111110
// check if it has images and no code

src/modules/tccpp/components/formatting-error-detection.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { strict as assert } from "assert";
55
import { M } from "../../../utils/debugging-and-logging.js";
66
import { colors, MINUTE } from "../../../common.js";
77
import { BotComponent } from "../../../bot-component.js";
8+
import { CommandSetBuilder } from "../../../command-abstractions/command-set-builder.js";
9+
import SkillRoles from "./skill-roles.js";
810
import { build_description, parse_out } from "../../../utils/strings.js";
911
import Code from "../../../components/code.js";
1012
import { SelfClearingMap, SelfClearingSet } from "../../../utils/containers.js";
@@ -121,10 +123,8 @@ export default class FormattingErrorDetection extends BotComponent {
121123
}
122124

123125
has_likely_format_errors(message: Discord.Message) {
124-
const has_skill_roles_other_than_beginner = message.member
125-
? this.wheatley.has_skill_roles_other_than_beginner(message.member)
126-
: false;
127-
if (has_skill_roles_other_than_beginner) {
126+
// trust Proficient+ members
127+
if (message.member && SkillRoles.find_highest_skill_role_index(message.member.roles.cache) >= 2) {
128128
return false;
129129
}
130130
return FormattingErrorDetection.has_likely_format_errors(message.content);

0 commit comments

Comments
 (0)