From 316e27a08d3efca465a5cdb0691b31b262d376b0 Mon Sep 17 00:00:00 2001 From: Rory& Date: Thu, 29 Oct 2026 12:55:17 +0100 Subject: [PATCH] Fix automod route and schemas --- assets/openapi.json | Bin 1117600 -> 1123866 bytes assets/schemas.json | Bin 526459 -> 531508 bytes scripts/schemaExclusions.json | 4 +- .../guilds/#guild_id/auto-moderation/rules.ts | 25 ++++--- src/database/entities/AutomodRule.ts | 9 ++- src/schemas/api/guilds/Automod.ts | 69 +++++++++++++----- .../uncategorised/AutomodRuleSchema.ts | 53 -------------- src/schemas/uncategorised/index.ts | 1 - 8 files changed, 74 insertions(+), 87 deletions(-) delete mode 100644 src/schemas/uncategorised/AutomodRuleSchema.ts diff --git a/assets/openapi.json b/assets/openapi.json index ee6ccf9e280e2a8b333b69dc6a0706475688881d..b14e83480bdf47e48a8f37d7ddcf15d8600b6842 100644 GIT binary patch delta 2231 zcmdT_Z%i9y7|&hrdUxe|cb#Pniq{S{h7%cVMCH$fV!MrhZTV9b=70uj+_KTq78XpV zn8o1yAgph~lg$u1V>V4pWX^bz#HAmWX@VieKi|we=hT@vS4_;JbHTeqp_XpJ=oddc zm%I1Hv+8|ezkMhYI1=bb zgVj5r_f6~+`psDkK{IDHYU^!7hyBiB{{Sr9((OX=y%Z{SF(|Od1@RW$kg~8}y%`2! z5kCO8t`JcaY^E?fELb=T#JnU4$*XubT)#?)Fk#p0Vd;H358ZYekl@OhJcd`G1Jx9o zci2Ga#YB)rY7ou4OfdVdJ|B{EI!5Dy%q?cB#UJtY`yz1i8dC+Mvy27ghY6pCM%S2J zNcQ0msRf)+HjU+j^#W0Hw|e($+M4_$M+2dLPsktkM^vHfeM3fyk2S5IVx|HXjgvR* z4|zg?XAU0ruai*@lzk!Cz;*~TOI7o_`);5ZFTxF6Pw70q5b{-Qic*^(Q9zQCaFGb| z2p!KxGQoiDI&m6=4~Z>MmW7M(Bd(|6#5@rNVG7#@NfCP#lz#71W_oN(|cUa!qeH>!JjFT_SE zJ=$KIzl=NN=-;rm8Ea|=&XOdW-rWi_xPJGB<+)8wQ)e`F=z3qr3G_hieq$;q0%tZ; zFQe*06HF}8Hq=v1qOMAQRl;NxuZ36?r!IU}c@h)S8?uJgKUDfGsoF_nF$izpBzC9< z!0mmoxChTmF+uX@&8zdDyW&NAU*uq5pkpNHFRgEOKi&k1uW1oxp5qHaxXhBuh1ja* znLrq#w}bo=u?yrdMPo-mh|`4-!;FgXH?Xp0bJ~XKRl8)Xbr`6tGUToLORdO&^A5RD zEz2R#P7~CXVDyUJ2GQTBikYakJ6(<*d$Y5xV?BItj^o7|QAx1kXRy0*u3A={_<-06 z$#Faje!9eEgZv)G!f1{%v)bwN@5IGU_8!T?atRRRw5%|kQQQDVh z3i^jaKDA+}rgf{P10|H3cnMdk(zC$CImW0SS1DS!zjAuQ{=UEfocCg8kllpvpQ$R$ zq^i|xZ?n5P8eGo*hp<17y#&daR1cfg({hbD8W-t(@Nq54!B!>!XbmlI*YQj6;(&AWWF{6Y*i7X{psk7MFJ|_j8 z>_trdd&2B(qaeNYi*XAfdAitFR~8LY=N)!jdz2K6Nq^7nJQC!fP$|g*7seny$2|z~ z1=a>j&FmD6dihh*%oKYR1J5IdTq#k`Ch-;gM_=kZ4KBc5;4VqAPuU7wl3(GMbZUNX mhMmuKko=A-S2%Hv`5E@_ojJ{w0bkEEW7Z;GvK9$1e*XuC??p}k delta 1290 zcmbW0dq`7J9LKr$+}F{0cLUoRZF-H;9-5;EHK?^nQm8Ci(8J1UvqjA|d{Iy`D3C&9 zb;U0h!Jzu5pp2rc(qp+Svj_X9{6mQS6GbH2U(sK6Hz6>zq(8oJ&iQ?QkMldfbNO{y z>`GaW*M;bMDq>`Sm{HeDY7sxhN_uD^y~yNvn;XM*hnnjA-f&GlYI;d2wryY?8IRme zq5AsJ@!-kL!J0sz&c8d<;BQPbhY@LKd3;dmE~K`wc?iE}k0P>$nUB*gY&JRv*?gSZ z!%A5Bp0#xOjPsiX)H})gI@o1MR&#|&R){t&F4(7YLR4caCeyrKK&!GhH8(be>i@HX zOX564D|xX?r4n`&(aKp=o%YFBsTob(!fg1d1se^V%;!gR9pw;sMC|Ysk#Z;xcn-%B zVl_%%inUnMM;0UIVAi9jj~tvuT~H##hO_---z;F`5?W6y)uN~O!a2%T!?QxFMMs=S z*r^y>V^xAyyBI)nfLJgvOcx=xn&e@JP4FNUC!#WCKz23<@pPCL{sCF18D0v1e8-oUE8^N&gGck#32E@rT5f?uLRTS0!tDC8D8Na-?s9*a^l8&sV{L$Xi~; zg}8wYl~4=X?08(!R5jPT?BLizOa~`&T=S6%@a3<7N@)jh|vkTz4 zXtHbSGdm`pkYX$k@`B!Sj|-bImf$k}bD{2~<#2xF_WzFgZt7Pk!}On`3cmZy>Na(d zGsGJb42ilrC|%jzrn#x1EJN9bOuFXI@xIcwKD6TYNX`RYk+W<}<_Yy%+43rGItvVo zq93)(SvzmxC^b9vmAU3E_8F;vM``o9&AQL6^ndyV Dh^*@o diff --git a/assets/schemas.json b/assets/schemas.json index d30f608f8170eced3dd839c0958d4fdad4f3b5b7..5d46e5e5c290165e652de116b51f7f13792731ff 100644 GIT binary patch delta 1609 zcmcIjT}%^M7|qPwxkI%$MU)>GZADZJv8_@|O&DmAu@Ku*O6!BJDUFPVT?H(&t7KzD zmn^$cwD!|}#%RPB4H{zuCiOuHi4VqPH|nzwy4mcDFY+$_%oIf}_?!4}lRIR+K zJKyt!<;G1*y3UFxn#jE0r`&^6Zbn;dX3EXgQqmCXRp7WQ0=A7m!Q?X;pN-yRDxz)k!FzZB?5j**ua(hcBfaoa67ZyrtVZ`3vlXAE1&Z$V zqyQ%B*I|DJyA8|^0ZzM0aC`@|2JOXMISyapSAt2Zz=@w(vrYi39HQV?)L2JMg?&SX zkT_L@=_8CHs`1Xeya*yuegFDqe|zA~a&c%QfO37IFRdIpRx5zcO?)3;B?30!bCYeeVZkYhf)s5!H6w%M~Nu)m(+;m7qJ zY;WKk`5)ZCe^pw94{FFRt=>G1QJhBKt&SSs2jcSEBnIb~7%*5%{9tiVkgehyyr zu>L8{8@pspF{WH&X9@cBo6Zq^81#Z6oVZ0IdY-9gr`p-wh3Oqk%}k^>zn!2my?@h) za9@vNZSb{h+7k84ZEYT}jLaFvhQ}pdz`tIS#YtxYwkOyCj$0T3gI%-?`~Tw#(B4AV zV)_zSio+st=^@X@05S%!3?9G}5>N5KCL)6Up&>QOSAUrQ86nGagtW$k7sR_RB4ia5T~; zhd$=X*#W@_|2LGrv19@3)!jIV`hTvws{=2}Y;n>_QmwS|>(ngEQPUdnggk+8b3p#i zf>uJX-k^_bHnfX!WMq?qL&Jg#e>)~*A*DHxcRifO8LOyIkK`x)UAKZ%;*+g34<9Fl SEc|l@yuA_5*!~uLwXXrxm^`}x delta 1070 zcmbVLUr19?80Xw`?>#fkJ8N@lYPSr9$T>7cB{%!GNTik)MvA%IFvGQ>PKXEwQARHt z-0D}>DwY{!43_`(-JpM<_G)zKEG$T z-{t5uqn+KN9mK8=-kp1lI{tfY%$yIt*w7|x|;E%3Dz;Yxa7Rh-EIa2P{P72UIXtY)lifci>sV!D;4A^M(G zXmjU`D68cqcP-M*2o(#(`qcz=qJK;Ve?S-+PPwpr3PU;NxPZTl;wK~JH?^KAeB;>)iQK& { const { guild_id } = req.params as { [key: string]: string }; - if (req.user_id !== req.body.creator_id) throw new HTTPError("You can't create a rule for someone else", 403); - if (guild_id !== req.body.guild_id) throw new HTTPError("You can't create a rule for another guild", 403); - - if (req.body.id) { - throw new HTTPError("You can't specify an ID for a new rule", 400); - } + if ("id" in req.body) throw new HTTPError("You can't specify an ID for a new rule", 400); + if ("user_id" in req.body && req.user_id !== req.body.creator_id) throw new HTTPError("You can't create a rule for someone else", 403); + if ("guild_id" in req.body && guild_id !== req.body.guild_id) throw new HTTPError("You can't create a rule for another guild", 403); const data = req.body as AutomodRuleSchema; const created = AutomodRule.create({ + ...{ exempt_roles: [], exempt_channels: [] }, ...data, creator: await User.findOneOrFail({ - where: { id: data.creator_id }, + where: { id: req.user_id }, }), guild_id: req.params.guild_id as string, }); + created.position = await AutomodRule.count({ where: { guild_id } }); + const savedRule = await AutomodRule.save(created); + console.log(savedRule); return res.json(savedRule); }, ); @@ -89,7 +90,7 @@ router.post( router.patch( "/:rule_id", route({ - // requestBody: "AutomodRuleSchema + requestBody: "AutomodRuleSchema", permission: "MANAGE_GUILD", responses: { 200: { @@ -110,9 +111,13 @@ router.patch( }); if (!rule) throw new HTTPError("Automod rule not found", 404); + if ("id" in req.body) throw new HTTPError("You can't specify an ID for a rule", 400); + if ("user_id" in req.body && req.user_id !== req.body.creator_id) throw new HTTPError("You can't modify a rule for someone else", 403); + if ("guild_id" in req.body && req.params.guild_id !== req.body.guild_id) throw new HTTPError("You can't modify a rule for another guild", 403); + const data = req.body as AutomodRuleSchema; - AutomodRule.merge(rule, data); + AutomodRule.merge(rule, data); const savedRule = await AutomodRule.save(rule); return res.json(savedRule); }, diff --git a/src/database/entities/AutomodRule.ts b/src/database/entities/AutomodRule.ts index 5f74ee9b6..93d60cd7b 100644 --- a/src/database/entities/AutomodRule.ts +++ b/src/database/entities/AutomodRule.ts @@ -17,7 +17,7 @@ */ import { BaseClass } from "./BaseClass"; -import { Entity, JoinColumn, ManyToOne, Column } from "typeorm"; +import { Entity, JoinColumn, ManyToOne, Column, RelationId } from "typeorm"; import { User } from "./User"; import { AutomodAction, AutomodRuleEventType, AutomodRuleTriggerMetadata, AutomodRuleTriggerType } from "@spacebar/schemas"; @@ -25,6 +25,10 @@ import { AutomodAction, AutomodRuleEventType, AutomodRuleTriggerMetadata, Automo name: "automod_rules", }) export class AutomodRule extends BaseClass { + @Column() + @RelationId((automod: AutomodRule) => automod.creator) + creator_id: string; + @JoinColumn({ name: "creator_id", foreignKeyConstraintName: "FK_automod_rule_creator_id" }) @ManyToOne(() => User, { onDelete: "CASCADE" }) creator: User; @@ -57,8 +61,7 @@ export class AutomodRule extends BaseClass { type: "jsonb", nullable: true, }) - trigger_metadata?: // this is null for "Block suspected spam content" - AutomodRuleTriggerMetadata; + trigger_metadata?: AutomodRuleTriggerMetadata | undefined; // may be undefined for trigger types that have no metadata @Column({ type: "jsonb", diff --git a/src/schemas/api/guilds/Automod.ts b/src/schemas/api/guilds/Automod.ts index 1c663776e..7f990edeb 100644 --- a/src/schemas/api/guilds/Automod.ts +++ b/src/schemas/api/guilds/Automod.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2025 Spacebar and Spacebar Contributors + Copyright (C) 2026 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -18,21 +18,59 @@ import { Snowflake } from "../../Identifiers"; -export type AutomodRuleTriggerMetadata = AutomodMentionSpamRule | AutomodSuspectedSpamRule | AutomodCommonlyFlaggedWordsRule | AutomodCustomWordsRule; +interface BaseAutomodRuleSchema { + enabled: boolean; + event_type: AutomodRuleEventType; + exempt_channels?: Snowflake[]; + exempt_roles?: Snowflake[]; + name: string; + actions: AutomodAction[]; +} -export class AutomodMentionSpamRule { +type WithTrigger = BaseAutomodRuleSchema & { + trigger_type: T; +} & M extends never + ? { trigger_metadata?: never } + : { trigger_metadata: M }; + +type KeywordRule = WithTrigger; +type HarmfulLinkRule = WithTrigger; +type SpamRule = WithTrigger; +type KeywordPresetRule = WithTrigger; +type MentionSpamRule = WithTrigger; +type UserProfileRule = WithTrigger; +type GuildPolicyRule = WithTrigger; + +export type AutomodRuleSchema = KeywordRule | HarmfulLinkRule | SpamRule | KeywordPresetRule | MentionSpamRule | UserProfileRule | GuildPolicyRule; + +export type AutomodRuleSchemaWithId = AutomodRuleSchema & { + id: Snowflake; + creator_id: Snowflake; + guild_id: Snowflake; + position: number; + exempt_roles?: Snowflake[]; +}; + +export type AutomodRuleTriggerMetadata = AutomodCustomWordsRuleMetadata | AutomodKeywordPresetRuleMetadata | AutomodMentionSpamRuleMetadata | AutomodUserProfileRuleMetadata; + +export interface AutomodMentionSpamRuleMetadata { mention_total_limit: number; mention_raid_protection_enabled: boolean; } -export class AutomodSuspectedSpamRule {} - -export class AutomodCommonlyFlaggedWordsRule { +export interface AutomodKeywordPresetRuleMetadata { allow_list: string[]; presets: AutomodKeywordPresetType[]; } -export class AutomodCustomWordsRule { +export interface AutomodCustomWordsRuleMetadata { + allow_list: string[]; + keyword_filter: string[]; + regex_patterns: string[]; +} + +// the same as custom words, but separate to allow divergence +export interface AutomodUserProfileRuleMetadata { allow_list: string[]; keyword_filter: string[]; regex_patterns: string[]; @@ -42,6 +80,7 @@ export enum AutomodRuleEventType { MESSAGE_SEND = 1, GUILD_MEMBER_EVENT = 2, } + export enum AutomodRuleTriggerType { KEYWORD = 1, HARMFUL_LINK = 2, @@ -68,27 +107,19 @@ export enum AutomodRuleActionType { export type AutomodAction = | { type: AutomodRuleActionType.BLOCK_MESSAGE; - metadata: { - custom_message?: string; - }; + metadata: { custom_message?: string }; } | { type: AutomodRuleActionType.SEND_ALERT_MESSAGE; - metadata: { - channel_id: Snowflake; - }; + metadata: { channel_id: Snowflake }; } | { type: AutomodRuleActionType.TIMEOUT_USER; - metadata: { - duration_seconds: number; - }; + metadata: { duration_seconds: number }; } | { type: AutomodRuleActionType.QUARANTINE_USER; - metadata: { - duration_seconds: number; - }; + metadata: { duration_seconds: number }; }; // TODO diff --git a/src/schemas/uncategorised/AutomodRuleSchema.ts b/src/schemas/uncategorised/AutomodRuleSchema.ts deleted file mode 100644 index 55625bf2e..000000000 --- a/src/schemas/uncategorised/AutomodRuleSchema.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2024 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -export interface AutomodMentionSpamRuleSchema { - mention_total_limit: number; - mention_raid_protection_enabled: boolean; -} - -// export interface AutomodSuspectedSpamRuleSchema {} -export type AutomodSuspectedSpamRuleSchema = Record; // hack to represent an empty object - -export interface AutomodCommonlyFlaggedWordsRuleSchema { - allow_list: [string]; - presets: [number]; -} - -export interface AutomodCustomWordsRuleSchema { - allow_list: [string]; - keyword_filter: [string]; - regex_patterns: [string]; -} - -export interface AutomodRuleSchema { - creator_id: string; - enabled: boolean; - event_type: number; // No idea... - exempt_channels: [string]; - exempt_roles: [string]; - guild_id: string; - name: string; - position: number; - trigger_type: number; //AutomodTriggerTypes - trigger_metadata: AutomodMentionSpamRuleSchema | AutomodSuspectedSpamRuleSchema | AutomodCommonlyFlaggedWordsRuleSchema | AutomodCustomWordsRuleSchema; -} - -export interface AutomodRuleSchemaWithId extends AutomodRuleSchema { - id: string; -} diff --git a/src/schemas/uncategorised/index.ts b/src/schemas/uncategorised/index.ts index ee1b01f57..d79e7fb6f 100644 --- a/src/schemas/uncategorised/index.ts +++ b/src/schemas/uncategorised/index.ts @@ -18,7 +18,6 @@ export * from "./ActivitySchema"; export * from "./ApplicationAuthorizeSchema"; -export * from "./AutomodRuleSchema"; export * from "./BackupCodesChallengeSchema"; export * from "./BanCreateSchema"; export * from "./BanModeratorSchema";