Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions src/components/anti-invite-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,28 +203,12 @@ export default class AntiInviteLinks extends BotComponent {
}
}

async member_is_proficient_or_higher(member: Discord.GuildMember | null) {
if (!member) {
return false;
}
const skill_roles = member.roles.cache.filter(role =>
Object.values(this.wheatley.skill_roles).some(skill_role => role.id == skill_role.id),
);
if (skill_roles.size > 1) {
const skill_role_ranks = Object.values(this.wheatley.skill_roles).map(role => role.id);
const proficient_index = skill_role_ranks.indexOf(this.wheatley.skill_roles.proficient.id);
assert(proficient_index !== -1);
return skill_roles.some(role => skill_role_ranks.indexOf(role.id) >= proficient_index);
}
return false;
}

async handle_message(message: Discord.Message) {
if (await this.wheatley.check_permissions(message.author, Discord.PermissionFlagsBits.ModerateMembers)) {
return;
}
const match = match_invite(message.content);
if (match && !(await this.is_allowed(match)) && !(await this.member_is_proficient_or_higher(message.member))) {
if (match && !(await this.is_allowed(match)) && !(await this.wheatley.is_established_member(message.author))) {
const quote = await this.utilities.make_quote_embeds(message);
await message.delete();
assert(!(message.channel instanceof Discord.PartialGroupDMChannel));
Expand Down
6 changes: 5 additions & 1 deletion src/components/moderation/modlogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export default class Modlogs extends BotComponent {
}

async case_info(command: TextBasedCommand, case_number: number) {
const moderation = await this.database.moderations.findOne({ case_number });
const moderation = await this.get_case(case_number);
if (moderation) {
await command.reply({
embeds: [
Expand All @@ -280,6 +280,10 @@ export default class Modlogs extends BotComponent {
}
}

async get_case(case_number: number) {
return await this.database.moderations.findOne({ case_number });
}

// TODO: Code duplication
async reply_with_error(command: TextBasedCommand, message: string) {
await command.replyOrFollowUp({
Expand Down
10 changes: 2 additions & 8 deletions src/components/moderation/modmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,8 @@ export default class Modmail extends BotComponent {
});
await this.log_action(interaction.member, "Modmail button spammed");
} else {
const member = await this.wheatley.guild.members.fetch(interaction.user.id);
const non_beginner_skill_roles = member.roles.cache.filter(role =>
Object.values(this.wheatley.skill_roles).some(
skill_role => role.id == skill_role.id && skill_role.name != "Beginner",
),
);
if (non_beginner_skill_roles.size > 0) {
// fast-path people who can read
if (await this.wheatley.is_established_member(interaction.user)) {
// fast-path established members
await interaction.deferReply({
ephemeral: true,
});
Expand Down
29 changes: 25 additions & 4 deletions src/components/notify-about-formerly-banned-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,44 @@ import { moderation_entry } from "./moderation/schemata.js";
import { unwrap } from "../utils/misc.js";
import { CommandSetBuilder } from "../command-abstractions/command-set-builder.js";
import LinkedAccounts from "./linked-accounts.js";
import { BotButton } from "../command-abstractions/button.js";

export type notify_plugin = {
maybe_create_button: (
member: Discord.GuildMember,
most_recent: moderation_entry,
) => Discord.ButtonBuilder | undefined;
};

export default class NotifyAboutFormerlyBannedUsers extends BotComponent {
private staff_action_log!: Discord.TextChannel;
private database = this.wheatley.database.create_proxy<{
moderations: moderation_entry;
}>();
private linked_accounts!: LinkedAccounts;
private plugins: notify_plugin[] = [];

override async setup(commands: CommandSetBuilder) {
this.staff_action_log = await this.utilities.get_channel(this.wheatley.channels.staff_action_log);
this.linked_accounts = unwrap(this.wheatley.components.get("LinkedAccounts")) as LinkedAccounts;
}

register_plugin(plugin: notify_plugin) {
this.plugins.push(plugin);
}

async alert(member: Discord.GuildMember, most_recent: moderation_entry, linked_accounts: Set<string>) {
const action = most_recent.type == "kick" ? "kicked" : "banned";

const description_parts = [
`User <@${member.user.id}> was previously ${action} on ${discord_timestamp(most_recent.issued_at)}`,
most_recent.reason ? `Reason: ${most_recent.reason}` : null,
];

if (linked_accounts.size > 0) {
const account_mentions = Array.from(linked_accounts)
.map(id => `<@${id}>`)
.join(", ");
description_parts.push(`⚠️ User has ${linked_accounts.size} linked accounts: ${account_mentions}`);
}

const embed = new Discord.EmbedBuilder()
.setColor(colors.alert_color)
.setAuthor({
Expand All @@ -48,7 +58,18 @@ export default class NotifyAboutFormerlyBannedUsers extends BotComponent {
text: `ID: ${member.id}`,
})
.setTimestamp();
await this.staff_action_log.send({ embeds: [embed] });
const components = (() => {
const buttons = this.plugins
.map(plugin => plugin.maybe_create_button(member, most_recent))
.filter(x => x !== undefined);
return buttons.length > 0
? [new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(...buttons)]
: undefined;
})();
await this.staff_action_log.send({
embeds: [embed],
components,
});
}

async find_most_recent_kick_or_ban(user_ids: string[]) {
Expand Down
63 changes: 2 additions & 61 deletions src/components/role-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import { unwrap } from "../utils/misc.js";
import { M } from "../utils/debugging-and-logging.js";
import { BotComponent } from "../bot-component.js";
import { CommandSetBuilder } from "../command-abstractions/command-set-builder.js";
import { skill_roles_order, skill_roles_order_id, Wheatley } from "../wheatley.js";
import { set_interval } from "../utils/node.js";
import { build_description } from "../utils/strings.js";

type user_role_entry = {
export type user_role_entry = {
user_id: string;
roles: string[];
last_known_skill_role: string | null;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a schema so changing it like this isn't great. I'll give a think about how to do it better.


type role_check = (member: Discord.GuildMember) => Promise<void>;
Expand All @@ -23,7 +21,6 @@ type role_update_listener = {
};

export default class RoleManager extends BotComponent {
private skill_role_log!: Discord.TextChannel;
private staff_member_log!: Discord.TextChannel;
interval: NodeJS.Timeout | null = null;

Expand All @@ -49,7 +46,6 @@ export default class RoleManager extends BotComponent {
}

override async setup(commands: CommandSetBuilder) {
this.skill_role_log = await this.utilities.get_channel(this.wheatley.channels.skill_role_log);
this.staff_member_log = await this.utilities.get_channel(this.wheatley.channels.staff_member_log);
}

Expand Down Expand Up @@ -85,20 +81,13 @@ export default class RoleManager extends BotComponent {
this.wheatley.roles.linked_github.id,
]);

this.register_role_check(this.check_skill_roles.bind(this));
this.register_role_update_listener(new Set(skill_roles_order_id), this.check_for_skill_role_bump.bind(this));

const check = () => {
this.check_members().catch(this.wheatley.critical_error.bind(this.wheatley));
};
check();
this.interval = set_interval(check, HOUR);
}

get_highest_skill_role(roles: string[]) {
return skill_roles_order_id.filter(id => roles.includes(id)).at(-1) ?? null;
}

async check_member_roles(member: Discord.GuildMember) {
for (const check of this.role_checks) {
await check(member);
Expand All @@ -108,10 +97,9 @@ export default class RoleManager extends BotComponent {
const diff = old_roles?.symmetricDifference(current_roles);
if (diff === undefined || diff.size > 0) {
const role_ids = current_roles.map(role => role.id);
const skill_role = this.get_highest_skill_role(member.roles.cache.map(role => role.id));
await this.database.user_roles.findOneAndUpdate(
{ user_id: member.id },
{ $set: skill_role ? { roles: role_ids, last_known_skill_role: skill_role } : { roles: role_ids } },
{ $set: { roles: role_ids } },
{ upsert: true },
);
const new_roles = new Set(role_ids);
Expand All @@ -124,53 +112,6 @@ export default class RoleManager extends BotComponent {
}
}

async check_skill_roles(member: Discord.GuildMember) {
const skill_roles = member.roles.cache.filter(role =>
Object.values(this.wheatley.skill_roles).some(skill_role => role.id == skill_role.id),
);
if (skill_roles.size > 1) {
M.log("removing duplicate skill roles for", member.user.tag);
skill_roles.sort((a, b) => b.rawPosition - a.rawPosition);
M.debug(skill_roles.map(x => x.name));
M.debug(skill_roles.map(x => x.name).slice(1));
for (const role of skill_roles.map(x => x).slice(1)) {
await member.roles.remove(role);
}
}
}

async check_for_skill_role_bump(member: Discord.GuildMember) {
const roles_entry = await this.database.user_roles.findOne({ user_id: member.id });
const last_known_skill_level =
roles_entry && roles_entry.last_known_skill_role
? this.wheatley.get_skill_role_index(roles_entry.last_known_skill_role)
: -1;
const current_skill_role = this.get_highest_skill_role(member.roles.cache.map(role => role.id));
const current_skill_level = current_skill_role ? this.wheatley.get_skill_role_index(current_skill_role) : -1;
if (
current_skill_level > skill_roles_order.indexOf("beginner") &&
current_skill_level > last_known_skill_level
) {
assert(current_skill_role);
M.log("Detected skill level increase for", member.user.tag);
await this.skill_role_log.send({
embeds: [
new Discord.EmbedBuilder()
.setAuthor({
name: member.displayName,
iconURL: member.displayAvatarURL(),
})
.setColor(unwrap(await this.wheatley.guild.roles.fetch(current_skill_role)).color)
.setDescription(
roles_entry?.last_known_skill_role
? `<@&${roles_entry.last_known_skill_role}> -> <@&${current_skill_role}>`
: `<@&${current_skill_role}>`,
),
],
});
}
}

async check_members() {
M.log("Starting role checks");
try {
Expand Down
5 changes: 2 additions & 3 deletions src/modules/tccpp/components/anti-screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ export default class AntiScreenshot extends BotComponent {
async anti_screenshot(starter_message: Discord.Message, thread: Discord.ThreadChannel) {
await delay(1000);
assert(starter_message);
assert(starter_message.member);
// trust people with skill roles
if (this.wheatley.has_skill_roles_other_than_beginner(starter_message.member)) {
// trust established members
if (await this.wheatley.is_established_member(starter_message.author)) {
return;
}
// check if it has images and no code
Expand Down
30 changes: 30 additions & 0 deletions src/modules/tccpp/components/establishment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as Discord from "discord.js";
import { unwrap } from "../../../utils/misc.js";
import { M } from "../../../utils/debugging-and-logging.js";
import { BotComponent } from "../../../bot-component.js";
import { CommandSetBuilder } from "../../../command-abstractions/command-set-builder.js";
import SkillRoles, { SkillLevel } from "./skill-roles.js";

export default class TheEstablishment extends BotComponent {
private skill_roles!: SkillRoles;

override async setup(commands: CommandSetBuilder) {
this.skill_roles = unwrap(this.wheatley.components.get("SkillRoles")) as SkillRoles;
this.wheatley.is_established_member = this.is_established_member.bind(this);
Comment thread
jeremy-rifkin marked this conversation as resolved.
}

private async is_established_member(
options: Discord.GuildMember | Discord.User | Discord.UserResolvable | Discord.FetchMemberOptions,
) {
const member = await this.wheatley.try_fetch_guild_member(options);
if (!member) {
return false;
}
return (
this.skill_roles.find_highest_skill_level(member) > SkillLevel.beginner ||
member.premiumSince != null ||
member.permissions.has(Discord.PermissionFlagsBits.MuteMembers) ||
member.permissions.has(Discord.PermissionFlagsBits.ModerateMembers)
);
}
}
12 changes: 8 additions & 4 deletions src/modules/tccpp/components/formatting-error-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { strict as assert } from "assert";
import { M } from "../../../utils/debugging-and-logging.js";
import { colors, MINUTE } from "../../../common.js";
import { BotComponent } from "../../../bot-component.js";
import SkillRoles, { SkillLevel } from "./skill-roles.js";
import { build_description, parse_out } from "../../../utils/strings.js";
import Code from "../../../components/code.js";
import { SelfClearingMap, SelfClearingSet } from "../../../utils/containers.js";
import * as dismark from "dismark";
import { CommandSetBuilder } from "../../../command-abstractions/command-set-builder.js";
import { unwrap } from "../../../utils/misc.js";
import { ButtonInteractionBuilder, BotButton } from "../../../command-abstractions/button.js";

const FAILED_CODE_BLOCK_RE = /^(?:"""?|'''?)(.+?)(?:"""?|'''?|$)/s;
Expand Down Expand Up @@ -40,12 +42,16 @@ class FailedCodeBlockRule extends dismark.Rule {
}

export default class FormattingErrorDetection extends BotComponent {
private skill_roles!: SkillRoles;

messaged = new SelfClearingSet<string>(10 * MINUTE);
// trigger message -> reply
replies = new SelfClearingMap<string, Discord.Message>(10 * MINUTE);
private dismiss_button!: BotButton<[string]>;

override async setup(commands: CommandSetBuilder) {
this.skill_roles = unwrap(this.wheatley.components.get("SkillRoles")) as SkillRoles;

this.dismiss_button = commands.add(
new ButtonInteractionBuilder("formatting_error_dismiss")
.add_user_id_metadata()
Expand Down Expand Up @@ -147,10 +153,8 @@ export default class FormattingErrorDetection extends BotComponent {
}

has_likely_format_errors(message: Discord.Message) {
const has_skill_roles_other_than_beginner = message.member
? this.wheatley.has_skill_roles_other_than_beginner(message.member)
: false;
if (has_skill_roles_other_than_beginner) {
// trust Proficient+ members
if (message.member && this.skill_roles.find_highest_skill_level(message.member) >= SkillLevel.proficient) {
return false;
}
return FormattingErrorDetection.has_likely_format_errors(message.content);
Expand Down
16 changes: 12 additions & 4 deletions src/modules/tccpp/components/permissions-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { strict as assert } from "assert";
import { M } from "../../../utils/debugging-and-logging.js";
import { HOUR } from "../../../common.js";
import { BotComponent } from "../../../bot-component.js";
import SkillRoles from "./skill-roles.js";
import { Wheatley } from "../../../wheatley.js";
import { unwrap } from "../../../utils/misc.js";
import { CommandSetBuilder } from "../../../command-abstractions/command-set-builder.js";

const categories_map = {
staff_logs: "1135927261472755712",
Expand Down Expand Up @@ -38,10 +40,16 @@ type permission_overwrites = Partial<Record<string, permissions_entry>>;
const SET_VOICE_STATUS_PERMISSION_BIT = 1n << 48n; // TODO: Replace once discord.js supports this in PermissionsBitField

export default class PermissionManager extends BotComponent {
private skill_roles!: SkillRoles;

category_permissions: Partial<Record<string, permission_overwrites>> = {};
channel_overwrites: Partial<Record<string, permission_overwrites>> = {};
dynamic_channel_overwrites: Partial<Record<string, permission_overwrites>> = {};

override async setup(commands: CommandSetBuilder) {
this.skill_roles = unwrap(this.wheatley.components.get("SkillRoles")) as SkillRoles;
}

setup_permissions_map() {
// permission sets
const write_permissions = [
Expand Down Expand Up @@ -140,10 +148,10 @@ export default class PermissionManager extends BotComponent {
},
[this.wheatley.roles.official_bot.id]: { allow: acive_voice_permissions },
[this.wheatley.roles.voice.id]: { allow: acive_voice_permissions },
[this.wheatley.skill_roles.intermediate.id]: { allow: acive_voice_permissions },
[this.wheatley.skill_roles.proficient.id]: { allow: acive_voice_permissions },
[this.wheatley.skill_roles.advanced.id]: { allow: acive_voice_permissions },
[this.wheatley.skill_roles.expert.id]: { allow: acive_voice_permissions },
[this.skill_roles.roles.intermediate.id]: { allow: acive_voice_permissions },
[this.skill_roles.roles.proficient.id]: { allow: acive_voice_permissions },
[this.skill_roles.roles.advanced.id]: { allow: acive_voice_permissions },
[this.skill_roles.roles.expert.id]: { allow: acive_voice_permissions },
[this.wheatley.roles.server_booster.id]: { allow: acive_voice_permissions },
[this.wheatley.roles.no_voice.id]: no_interaction_at_all,
[this.wheatley.roles.no_off_topic.id]: no_interaction_at_all,
Expand Down
Loading