From 74f6224091fcaadf9d1fbde7333af1ece457b54f Mon Sep 17 00:00:00 2001 From: Ajay Date: Thu, 10 Apr 2025 02:26:09 -0400 Subject: [PATCH] Add new user limit per 5 mins --- src/routes/voteOnSponsorTime.ts | 11 +------- src/utils/permissions.ts | 47 ++++++++++++++++++++++----------- src/utils/redis.ts | 10 +++++++ 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 1eb7c9a..bcc9514 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { Logger } from "../utils/logger"; import { isUserVIP } from "../utils/isUserVIP"; import { isUserTempVIP } from "../utils/isUserTempVIP"; -import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi"; +import { getMaxResThumbnail } from "../utils/youtubeApi"; import { db, privateDB } from "../databases/databases"; import { dispatchEvent, getVoteAuthor, getVoteAuthorRaw } from "../utils/webhookUtils"; import { getFormattedTime } from "../utils/getFormattedTime"; @@ -17,7 +17,6 @@ import { getVideoDetails, videoDetails } from "../utils/getVideoDetails"; import { deleteLockCategories } from "./deleteLockCategories"; import { acquireLock } from "../utils/redisLock"; import { checkBanStatus } from "../utils/checkBan"; -import { canVote } from "../utils/permissions"; const voteTypes = { normal: 0, @@ -343,14 +342,6 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID const nonAnonUserID = await getHashCache(paramUserID); const userID = await getHashCache(paramUserID + UUID); - const permission = await canVote(nonAnonUserID); - if (!permission.canSubmit) { - return { - status: 403, - message: permission.reason - }; - } - //hash the ip 5000 times so no one can get it from the database const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress); diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index eeba3e2..3fae999 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -5,6 +5,7 @@ import { Feature, HashedUserID } from "../types/user.model"; import { hasFeature } from "./features"; import { isUserVIP } from "./isUserVIP"; import { oneOf } from "./promise"; +import redis from "./redis"; import { getReputation } from "./reputation"; import { getServerConfig } from "./serverConfig"; @@ -20,7 +21,8 @@ async function lowDownvotes(userID: HashedUserID): Promise { return result.submissionCount > 5 && result.downvotedSubmissions / result.submissionCount < 0.10; } -async function oldSubmitter(userID: HashedUserID): Promise { +const fiveMinutes = 5 * 60 * 1000; +async function oldSubmitterOrAllowed(userID: HashedUserID): Promise { const submitterThreshold = await getServerConfig("old-submitter-block-date"); if (!submitterThreshold) { return true; @@ -29,10 +31,22 @@ async function oldSubmitter(userID: HashedUserID): Promise { const result = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ? AND "timeSubmitted" < ?` , [userID, parseInt(submitterThreshold)], { useReplica: true }); - return result.submissionCount >= 1; + const isOldSubmitter = result.submissionCount >= 1; + if (!isOldSubmitter) { + await redis.zRemRangeByScore("submitters", "-inf", Date.now() - fiveMinutes); + const last5MinUsers = await redis.zCard("submitters"); + const maxUsers = await getServerConfig("max-users-per-minute"); + + if (maxUsers && last5MinUsers < parseInt(maxUsers)) { + await redis.zAdd("submitters", { score: Date.now(), value: userID }); + return true; + } + } + + return isOldSubmitter; } -async function oldDeArrowSubmitter(userID: HashedUserID): Promise { +async function oldDeArrowSubmitterOrAllowed(userID: HashedUserID): Promise { const submitterThreshold = await getServerConfig("old-submitter-block-date"); if (!submitterThreshold) { return true; @@ -41,7 +55,19 @@ async function oldDeArrowSubmitter(userID: HashedUserID): Promise { const result = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "titles" WHERE "userID" = ? AND "timeSubmitted" < 1743827196000` , [userID, parseInt(submitterThreshold)], { useReplica: true }); - return result.submissionCount >= 1; + const isOldSubmitter = result.submissionCount >= 1; + if (!isOldSubmitter) { + await redis.zRemRangeByScore("submittersDeArrow", "-inf", Date.now() - fiveMinutes); + const last5MinUsers = await redis.zCard("submittersDeArrow"); + const maxUsers = await getServerConfig("max-users-per-minute-dearrow"); + + if (maxUsers && last5MinUsers < parseInt(maxUsers)) { + await redis.zAdd("submittersDeArrow", { score: Date.now(), value: userID }); + return true; + } + } + + return isOldSubmitter; } export async function canSubmit(userID: HashedUserID, category: Category): Promise { @@ -66,16 +92,7 @@ export async function canSubmit(userID: HashedUserID, category: Category): Promi export async function canSubmitGlobal(userID: HashedUserID): Promise { return { canSubmit: await oneOf([isUserVIP(userID), - oldSubmitter(userID) - ]), - reason: "We are currently experiencing a mass spam attack, we are restricting submissions for now" - }; -} - -export async function canVote(userID: HashedUserID): Promise { - return { - canSubmit: await oneOf([isUserVIP(userID), - oldSubmitter(userID) + oldSubmitterOrAllowed(userID) ]), reason: "We are currently experiencing a mass spam attack, we are restricting submissions for now" }; @@ -84,7 +101,7 @@ export async function canVote(userID: HashedUserID): Promise { export async function canSubmitDeArrow(userID: HashedUserID): Promise { return { canSubmit: await oneOf([isUserVIP(userID), - oldDeArrowSubmitter(userID) + oldDeArrowSubmitterOrAllowed(userID) ]), reason: "We are currently experiencing a mass spam attack, we are restricting submissions for now" }; diff --git a/src/utils/redis.ts b/src/utils/redis.ts index 462a37d..112361c 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -9,6 +9,7 @@ import { Postgres } from "../databases/Postgres"; import { compress, uncompress } from "lz4-napi"; import { LRUCache } from "lru-cache"; import { shouldClientCacheKey } from "./redisKeys"; +import { ZMember } from "@redis/client/dist/lib/commands/generic-transformers"; export interface RedisStats { activeRequests: number; @@ -35,6 +36,9 @@ interface RedisSB { sendCommand(args: RedisCommandArguments, options?: RedisClientOptions): Promise; ttl(key: RedisCommandArgument): Promise; quit(): Promise; + zRemRangeByScore(key: string, min: number | RedisCommandArgument, max: number | RedisCommandArgument): Promise; + zAdd(key: string, members: ZMember | ZMember[]): Promise; + zCard(key: string): Promise; } let exportClient: RedisSB = { @@ -49,6 +53,9 @@ let exportClient: RedisSB = { sendCommand: () => Promise.resolve(null), quit: () => Promise.resolve(null), ttl: () => Promise.resolve(null), + zRemRangeByScore: () => Promise.resolve(null), + zAdd: () => Promise.resolve(null), + zCard: () => Promise.resolve(null) }; let lastClientFail = 0; @@ -308,6 +315,9 @@ if (config.redis?.enabled) { .then((reply) => resolve(reply)) .catch((err) => reject(err)) ); + exportClient.zRemRangeByScore = client.zRemRangeByScore.bind(client); + exportClient.zAdd = client.zAdd.bind(client); + exportClient.zCard = client.zCard.bind(client); /* istanbul ignore next */ client.on("error", function(error) { lastClientFail = Date.now();