@@ -2,35 +2,146 @@ import { strict as assert } from "assert";
22
33import * as Discord from "discord.js" ;
44
5+ import { M } from "../utils/debugging-and-logging.js" ;
56import { BotComponent } from "../bot-component.js" ;
67import { departialize } from "../utils/discord.js" ;
78import { 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
913const INVITE_RE =
1014 / (?: (?: d i s c o r d (?: a p p ) ? | d i s b o a r d ) \. (?: g g | (?: c o m | o r g | m e ) \/ (?: i n v i t e | s e r v e r \/ j o i n ) ) | (?< ! \w ) \. g g ) \/ ( \S + ) / i;
1115
12- const whitelist = [
13- "tccpp" ,
14- "python" ,
15- "csharp" ,
16- "bVTPVpYVcv" , // cuda
17- "Eb7P3wH" , // graphics
18- ] ;
19-
2016export 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+
2528export 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