Skip to content

Commit 9512600

Browse files
Replace hardcoded list of allowed server invites
1 parent d01ee7b commit 9512600

1 file changed

Lines changed: 123 additions & 12 deletions

File tree

src/components/anti-invite-links.ts

Lines changed: 123 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,146 @@ import { strict as assert } from "assert";
22

33
import * as Discord from "discord.js";
44

5+
import { M } from "../utils/debugging-and-logging.js";
56
import { BotComponent } from "../bot-component.js";
67
import { departialize } from "../utils/discord.js";
78
import { CommandSetBuilder } from "../command-abstractions/command-set-builder.js";
9+
import { Wheatley } from "../wheatley.js";
10+
import { EarlyReplyMode, TextBasedCommandBuilder } from "../command-abstractions/text-based-command-builder.js";
11+
import { TextBasedCommand } from "../command-abstractions/text-based-command.js";
812

913
const INVITE_RE =
1014
/(?:(?:discord(?:app)?|disboard)\.(?:gg|(?:com|org|me)\/(?:invite|server\/join))|(?<!\w)\.gg)\/(\S+)/i;
1115

12-
const whitelist = [
13-
"tccpp",
14-
"python",
15-
"csharp",
16-
"bVTPVpYVcv", // cuda
17-
"Eb7P3wH", // graphics
18-
];
19-
2016
export function match_invite(content: string): string | null {
2117
const match = content.match(INVITE_RE);
2218
return match ? match[1] : null;
2319
}
2420

21+
type allowed_invite_entry = {
22+
code: string;
23+
guild_id: string;
24+
guild_name: string;
25+
guild_icon: string;
26+
};
27+
2528
export default class AntiInviteLinks extends BotComponent {
26-
static override get is_freestanding() {
27-
return true;
28-
}
29+
private allowed_invites = new Set<string>();
2930

3031
private staff_flag_log!: Discord.TextChannel;
3132

33+
private database = this.wheatley.database.create_proxy<{
34+
allowed_invites: allowed_invite_entry;
35+
}>();
36+
3237
override async setup(commands: CommandSetBuilder) {
3338
this.staff_flag_log = await this.utilities.get_channel(this.wheatley.channels.staff_flag_log);
39+
40+
commands.add(
41+
new TextBasedCommandBuilder("allowed-invites", EarlyReplyMode.ephemeral)
42+
.set_permissions(Discord.PermissionFlagsBits.ModerateMembers)
43+
.set_description("manage allowed server invites")
44+
.add_subcommand(
45+
new TextBasedCommandBuilder("add", EarlyReplyMode.ephemeral)
46+
.set_permissions(Discord.PermissionFlagsBits.Administrator)
47+
.set_description("add allowed invite code")
48+
.add_string_option({
49+
title: "code",
50+
description: "code to add",
51+
required: true,
52+
})
53+
.set_handler(this.handle_add_remove.bind(this, true)),
54+
)
55+
.add_subcommand(
56+
new TextBasedCommandBuilder("remove", EarlyReplyMode.ephemeral)
57+
.set_permissions(Discord.PermissionFlagsBits.Administrator)
58+
.set_description("remove allowed invite code")
59+
.add_string_option({
60+
title: "code",
61+
description: "code to remove",
62+
required: true,
63+
})
64+
.set_handler(this.handle_add_remove.bind(this, false)),
65+
)
66+
.add_subcommand(
67+
new TextBasedCommandBuilder("list", EarlyReplyMode.ephemeral)
68+
.set_description("list all allowed server invites")
69+
.set_handler(this.handle_list.bind(this)),
70+
),
71+
);
72+
}
73+
74+
override async on_ready() {
75+
this.allowed_invites = new Set((await this.database.allowed_invites.find().toArray()).map(e => e.code));
76+
}
77+
78+
private static build_guild_embed(entry: allowed_invite_entry) {
79+
return new Discord.EmbedBuilder()
80+
.setAuthor({
81+
name: entry.guild_name,
82+
url: `https://discord.gg/${entry.code}`,
83+
iconURL: `https://cdn.discordapp.com/icons/${entry.guild_id}/${entry.guild_icon}.png`,
84+
})
85+
.setFooter({ text: entry.code });
86+
}
87+
88+
private async handle_add_remove(add: boolean, command: TextBasedCommand, code: string) {
89+
if (add) {
90+
if (this.allowed_invites.has(code)) {
91+
await command.react("🤷", true);
92+
return;
93+
}
94+
M.log("Adding ", code, " to allowed invites");
95+
const server_info = await (await fetch(`https://discord.com/api/v10/invites/${code}`)).json();
96+
if (server_info.code != code) {
97+
await command.replyOrFollowUp(`${this.wheatley.emoji.error} not a valid invite`, true);
98+
return;
99+
}
100+
if (server_info.expires != null) {
101+
await command.replyOrFollowUp(`${this.wheatley.emoji.error} not a permanent invite`, true);
102+
return;
103+
}
104+
const res = await this.database.allowed_invites.findOneAndUpdate(
105+
{ code: code },
106+
{
107+
$set: {
108+
code: code,
109+
guild_id: server_info.guild_id,
110+
guild_name: server_info.guild.name,
111+
guild_icon: server_info.guild.icon,
112+
},
113+
},
114+
{ upsert: true, returnDocument: "after" },
115+
);
116+
if (res == null) {
117+
await command.replyOrFollowUp(`${this.wheatley.emoji.error} database update failed`, true);
118+
return;
119+
}
120+
this.allowed_invites.add(code);
121+
await command.replyOrFollowUp({
122+
embeds: [AntiInviteLinks.build_guild_embed(res)],
123+
});
124+
} else {
125+
if (!this.allowed_invites.has(code)) {
126+
await command.react("🤷", true);
127+
return;
128+
}
129+
M.log("Removing ", code, " from allowed invites");
130+
const res = await this.database.allowed_invites.findOneAndDelete({ code: code });
131+
if (res == null) {
132+
await command.replyOrFollowUp(`${this.wheatley.emoji.error} database update failed`, true);
133+
return;
134+
}
135+
this.allowed_invites.delete(code);
136+
await command.react(this.wheatley.emoji.success, true);
137+
}
138+
}
139+
140+
private async handle_list(command: TextBasedCommand) {
141+
const codes = await this.database.allowed_invites.find().toArray();
142+
await command.reply({
143+
embeds: codes.map(AntiInviteLinks.build_guild_embed),
144+
});
34145
}
35146

36147
async member_is_proficient_or_higher(member: Discord.GuildMember | null) {
@@ -54,7 +165,7 @@ export default class AntiInviteLinks extends BotComponent {
54165
return;
55166
}
56167
const match = match_invite(message.content);
57-
if (match && !whitelist.includes(match) && !(await this.member_is_proficient_or_higher(message.member))) {
168+
if (match && !this.allowed_invites.has(match) && !(await this.member_is_proficient_or_higher(message.member))) {
58169
const quote = await this.utilities.make_quote_embeds([message]);
59170
await message.delete();
60171
assert(!(message.channel instanceof Discord.PartialGroupDMChannel));

0 commit comments

Comments
 (0)