diff --git a/packages/backend/src/misc/convert-milliseconds.ts b/packages/backend/src/misc/convert-milliseconds.ts new file mode 100644 index 0000000000..d8c163ffda --- /dev/null +++ b/packages/backend/src/misc/convert-milliseconds.ts @@ -0,0 +1,17 @@ +export function convertMilliseconds(ms: number) { + let seconds = Math.round(ms / 1000); + let minutes = Math.round(seconds / 60); + let hours = Math.round(minutes / 60); + const days = Math.round(hours / 24); + seconds %= 60; + minutes %= 60; + hours %= 24; + + const result = []; + if (days > 0) result.push(`${days} day(s)`); + if (hours > 0) result.push(`${hours} hour(s)`); + if (minutes > 0) result.push(`${minutes} minute(s)`); + if (seconds > 0) result.push(`${seconds} second(s)`); + + return result.join(", "); +} diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 45471ed564..0a1027b835 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -66,8 +66,11 @@ export default async ( limit as IEndpointMeta["limit"] & { key: NonNullable }, limitActor, ).catch((e) => { + const remainingTime = e.remainingTime + ? `Please try again in ${e.remainingTime}.` + : "Please try again later."; throw new ApiError({ - message: "Rate limit exceeded. Please try again later.", + message: `Rate limit exceeded. ${remainingTime}`, code: "RATE_LIMIT_EXCEEDED", id: "d5826d14-3982-4d2e-8011-b9e9f02499ef", httpStatusCode: 429, @@ -94,7 +97,7 @@ export default async ( } if (ep.meta.requireAdmin && !user!.isAdmin) { - throw new ApiError(accessDenied, { reason: "You are not the admin." }); + throw new ApiError(accessDenied, { reason: "You are not an admin." }); } if (ep.meta.requireModerator && !isModerator) { diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts index dd005ad136..a661ed0e95 100644 --- a/packages/backend/src/server/api/limiter.ts +++ b/packages/backend/src/server/api/limiter.ts @@ -3,6 +3,7 @@ import { CacheableLocalUser, User } from "@/models/entities/user.js"; import Logger from "@/services/logger.js"; import { redisClient } from "../../db/redis.js"; import type { IEndpointMeta } from "./endpoints.js"; +import { convertMilliseconds } from "@/misc/convert-milliseconds.js"; const logger = new Logger("limiter"); @@ -76,7 +77,10 @@ export const limiter = ( ); if (info.remaining === 0) { - reject("RATE_LIMIT_EXCEEDED"); + reject({ + message: "RATE_LIMIT_EXCEEDED", + remainingTime: convertMilliseconds(info.resetMs), + }); } else { ok(); } diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts index 0fe229d88e..9e7c77c0f2 100644 --- a/packages/backend/src/server/api/openapi/errors.ts +++ b/packages/backend/src/server/api/openapi/errors.ts @@ -3,7 +3,7 @@ export const errors = { INVALID_PARAM: { value: { error: { - message: "Invalid param.", + message: "Invalid parameter.", code: "INVALID_PARAM", id: "3d81ceae-475f-4600-b2a8-2bc116157532", }, @@ -25,8 +25,7 @@ export const errors = { AUTHENTICATION_FAILED: { value: { error: { - message: - "Authentication failed. Please ensure your token is correct.", + message: "Authentication failed.", code: "AUTHENTICATION_FAILED", id: "b0a7f5f8-dc2f-4171-b91f-de88ad238e14", }, @@ -38,7 +37,7 @@ export const errors = { value: { error: { message: - "You sent a request to Calc, Calckey's resident stoner furry, instead of the server.", + "You sent a request to Calc instead of the server. How did this happen?", code: "I_AM_CALC", id: "60c46cd1-f23a-46b1-bebe-5d2b73951a84", },