diff --git a/assets/openapi.json b/assets/openapi.json index 463269130..49e275458 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index f7b875759..3bdba9d7a 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/AutomodTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/AutomodTests.cs index d52df2beb..31c2001ea 100644 --- a/extra/admin-api/Tests/Spacebar.Tests/Tests/AutomodTests.cs +++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/AutomodTests.cs @@ -107,6 +107,7 @@ public class AutomodTests(ITestOutputHelper testOutputHelper, TestFixture fixtur automodRule.Name = Guid.NewGuid().ToString(); + _testOutputHelper.WriteLine(automodRule.ToJson()); var r = await Assert.HttpSuccess(await Client.ApiHttpClient.PostAsJsonAsync( $"guilds/{Guild.Id}/auto-moderation/rules", automodRule, diff --git a/src/api/middlewares/ErrorHandler.ts b/src/api/middlewares/ErrorHandler.ts index 57073c2fa..901fbcea1 100644 --- a/src/api/middlewares/ErrorHandler.ts +++ b/src/api/middlewares/ErrorHandler.ts @@ -20,6 +20,7 @@ import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server/HTTPError"; import { ApiError, FieldError, FieldErrors } from "@spacebar/util"; import { StringLengthOutOfBoundsException } from "@spacebar/extensions"; +import { getInvalidSchemaValues } from "@spacebar/schemas"; const EntityNotFoundErrorRegex = /"(\w+)"/; export function ErrorHandler(error: Error & { type?: string }, req: Request, res: Response, next: NextFunction) { @@ -75,7 +76,14 @@ export function ErrorHandler(error: Error & { type?: string }, req: Request, res if (httpcode > 511) httpcode = 400; - res.status(httpcode).json({ code, message, errors, _ajvErrors, request: `${req.method} ${req.url}` }); + res.status(httpcode).json({ + code, + message, + errors, + _ajvErrors, + _ajvResolved: _ajvErrors == undefined ? undefined : getInvalidSchemaValues(req.body, _ajvErrors), + request: `${req.method} ${req.url}`, + }); } catch (error) { console.error(`[Internal Server Error] 500`, error); return res.status(500).json({ code: 500, message: `Internal server error while handling error`, request: `${req.method} ${req.url}` }); diff --git a/src/api/middlewares/Route.ts b/src/api/middlewares/Route.ts index 4b7ccb12e..5341f100b 100644 --- a/src/api/middlewares/Route.ts +++ b/src/api/middlewares/Route.ts @@ -177,7 +177,10 @@ export function route(opts: RouteOptions) { message: x.message || "", }), ); - if (process.env.LOG_VALIDATION_ERRORS) console.log(`[VALIDATION ERROR] ${req.method} ${req.originalUrl} - SCHEMA='${opts.requestBody}' -`, validate?.errors); + if (process.env.LOG_VALIDATION_ERRORS) { + console.log(`[VALIDATION ERROR] ${req.method} ${req.originalUrl} - SCHEMA='${opts.requestBody}' -`, validate?.errors); + if (process.env.LOG_VALIDATION_ERROR_SCHEMAS) console.log(validate.schema); + } throw FieldErrors(fields, validate.errors!); } } diff --git a/src/schemas/Validator.ts b/src/schemas/Validator.ts index c83e16436..73672c183 100644 --- a/src/schemas/Validator.ts +++ b/src/schemas/Validator.ts @@ -52,7 +52,7 @@ export const ajv = (function () { strict: true, strictRequired: true, allowUnionTypes: true, - discriminator: true + discriminator: true, }); addFormats(_ajv); @@ -70,23 +70,28 @@ export function validateSchema(schema: string, data: G): G { return data; } -export function getInvalidSchemaValues(data: object, errors: typeof ajv.errors) { +export function getInvalidSchemaValues(data: object, errors: typeof ajv.errors): { instancePath: string; value: unknown; keyword: string }[] { if (errors == null) return []; - return errors.map((e) => { - let instancePath = e.instancePath; - if (e.keyword == "additionalProperties") instancePath += `/${e.params.additionalProperty}`; + return errors + .map((e) => { + let instancePath = e.instancePath; + if (e.keyword == "additionalProperties") instancePath += `/${e.params.additionalProperty}`; + else if (e.keyword == "required") return { instancePath: instancePath + `/${e.params.missingProperty}`, value: "<>", keyword: e.keyword }; + else if (e.keyword == "anyOf") return { instancePath: instancePath, value: "<>", keyword: e.keyword }; - console.log("Resolving:", { instancePath, eInstancePath: e.instancePath, eKeyword: e.keyword, eParams: e.params }); - return getInvalidSchemaValue(data, instancePath); - }); + console.log("Resolving:", { instancePath, eInstancePath: e.instancePath, eKeyword: e.keyword, eParams: e.params }); + return { ...getInvalidSchemaValue(data, instancePath), keyword: e.keyword }; + }) + .filter((x) => x != undefined); } -function getInvalidSchemaValue(data: object, instancePath: string) { +function getInvalidSchemaValue(data: object, instancePath: string): { instancePath: string; value: unknown } { const actualInstancePath = instancePath.startsWith("/") ? instancePath.slice(1) : instancePath; - if (actualInstancePath.includes("/")) { + if (actualInstancePath.trim().length > 0) { + // includes("/") const nextProp = actualInstancePath.split("/")[0]; const nextPath = actualInstancePath.split("/").slice(1).join("/"); - return getInvalidSchemaValue((>data)[nextProp], nextPath); + return { instancePath, value: getInvalidSchemaValue((>data)[nextProp], nextPath).value }; } return { instancePath, value: data }; } diff --git a/src/schemas/api/guilds/Automod.ts b/src/schemas/api/guilds/Automod.ts index 7f990edeb..ff673810c 100644 --- a/src/schemas/api/guilds/Automod.ts +++ b/src/schemas/api/guilds/Automod.ts @@ -27,19 +27,13 @@ interface BaseAutomodRuleSchema { actions: AutomodAction[]; } -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; +type KeywordRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.KEYWORD; trigger_metadata: AutomodCustomWordsRuleMetadata }; +type KeywordPresetRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.KEYWORD_PRESET; trigger_metadata: AutomodKeywordPresetRuleMetadata }; +type MentionSpamRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.MENTION_SPAM; trigger_metadata: AutomodMentionSpamRuleMetadata }; +type UserProfileRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.USER_PROFILE; trigger_metadata: AutomodUserProfileRuleMetadata }; +type HarmfulLinkRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.HARMFUL_LINK; trigger_metadata?: never }; +type GuildPolicyRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.GUILD_POLICY; trigger_metadata?: never }; +type SpamRule = BaseAutomodRuleSchema & { trigger_type: AutomodRuleTriggerType.SPAM; trigger_metadata?: never }; export type AutomodRuleSchema = KeywordRule | HarmfulLinkRule | SpamRule | KeywordPresetRule | MentionSpamRule | UserProfileRule | GuildPolicyRule;