From 4225d9b3b3a1c69a4fad5f1b15a22a424bfe38ab Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Tue, 8 Jun 2021 20:20:05 -0400 Subject: [PATCH 1/6] Silently reject votes --- src/routes/voteOnSponsorTime.ts | 35 +++++++++++++++++++++++++++------ src/types/config.model.ts | 1 + test/cases/voteOnSponsorTime.ts | 8 ++++---- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index ce59172..36e23a0 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -19,9 +19,17 @@ const voteTypes = { incorrect: 1, }; +enum VoteWebhookType { + Normal, + Rejected +} + interface FinalResponse { + blockVote: boolean, finalStatus: number - finalMessage: string + finalMessage: string, + webhookType: VoteWebhookType, + webhookMessage: string } interface VoteData { @@ -52,7 +60,15 @@ async function sendWebhooks(voteData: VoteData) { if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) { let webhookURL: string = null; if (voteData.voteTypeEnum === voteTypes.normal) { - webhookURL = config.discordReportChannelWebhookURL; + switch (voteData.finalResponse.webhookType) { + case VoteWebhookType.Normal: + webhookURL = config.discordReportChannelWebhookURL; + break; + case VoteWebhookType.Rejected: + webhookURL = config.discordFailedReportChannelWebhookURL; + break; + } + } else if (voteData.voteTypeEnum === voteTypes.incorrect) { webhookURL = config.discordCompletelyIncorrectReportWebhookURL; } @@ -114,7 +130,9 @@ async function sendWebhooks(voteData: VoteData) { getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime), "color": 10813440, "author": { - "name": voteData.finalResponse?.finalMessage ?? getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission), + "name": voteData.finalResponse?.webhookMessage ?? + voteData.finalResponse?.finalMessage ?? + getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission), }, "thumbnail": { "url": getMaxResThumbnail(data) || "", @@ -252,8 +270,11 @@ export async function voteOnSponsorTime(req: Request, res: Response) { // To force a non 200, change this early let finalResponse: FinalResponse = { + blockVote: false, finalStatus: 200, - finalMessage: null + finalMessage: null, + webhookType: VoteWebhookType.Normal, + webhookMessage: null } //x-forwarded-for if this server is behind a proxy @@ -276,8 +297,9 @@ export async function voteOnSponsorTime(req: Request, res: Response) { ' where "UUID" = ?', [UUID])); if (await isSegmentLocked() || await isVideoLocked()) { - finalResponse.finalStatus = 403; - finalResponse.finalMessage = "Vote rejected: A moderator has decided that this segment is correct" + finalResponse.blockVote = true; + finalResponse.webhookType = VoteWebhookType.Normal + finalResponse.webhookMessage = "Vote rejected: A moderator has decided that this segment is correct" } } @@ -384,6 +406,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) { && (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined && (await privateDB.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID])) === undefined && (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID])) === undefined) + && !finalResponse.blockVote && finalResponse.finalStatus === 200; if (ableToVote) { diff --git a/src/types/config.model.ts b/src/types/config.model.ts index f5614d9..dc16d0d 100644 --- a/src/types/config.model.ts +++ b/src/types/config.model.ts @@ -8,6 +8,7 @@ export interface SBSConfig { adminUserID: string; newLeafURLs?: string[]; discordReportChannelWebhookURL?: string; + discordFailedReportChannelWebhookURL?: string; discordFirstTimeSubmissionsWebhookURL?: string; discordCompletelyIncorrectReportWebhookURL?: string; neuralBlockURL?: string; diff --git a/test/cases/voteOnSponsorTime.ts b/test/cases/voteOnSponsorTime.ts index f649d12..b2cbc27 100644 --- a/test/cases/voteOnSponsorTime.ts +++ b/test/cases/voteOnSponsorTime.ts @@ -446,10 +446,10 @@ describe('voteOnSponsorTime', () => { + "/api/voteOnSponsorTime?userID=randomID&UUID=no-sponsor-segments-uuid-0&type=0") .then(async res => { let row = await db.prepare('get', `SELECT "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, ["no-sponsor-segments-uuid-0"]); - if (res.status === 403 && row.votes === 2) { + if (res.status === 200 && row.votes === 2) { done(); } else { - done("Status code was " + res.status + " instead of 403, row was " + JSON.stringify(row)); + done("Status code was " + res.status + " instead of 200, row was " + JSON.stringify(row)); } }) .catch(err => done(err)); @@ -474,10 +474,10 @@ describe('voteOnSponsorTime', () => { + "/api/voteOnSponsorTime?userID=randomID&UUID=no-sponsor-segments-uuid-0&category=outro") .then(async res => { let row = await db.prepare('get', `SELECT "category" FROM "sponsorTimes" WHERE "UUID" = ?`, ["no-sponsor-segments-uuid-0"]); - if (res.status === 403 && row.category === "sponsor") { + if (res.status === 200 && row.category === "sponsor") { done(); } else { - done("Status code was " + res.status + " instead of 403, row was " + JSON.stringify(row)); + done("Status code was " + res.status + " instead of 200, row was " + JSON.stringify(row)); } }) .catch(err => done(err)); From 344e680fe36f05a288c8ca372c2a26658d6ec382 Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Wed, 9 Jun 2021 15:14:31 -0400 Subject: [PATCH 2/6] Fix rejections not being seperated --- src/routes/voteOnSponsorTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 36e23a0..0aec481 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -298,7 +298,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) { if (await isSegmentLocked() || await isVideoLocked()) { finalResponse.blockVote = true; - finalResponse.webhookType = VoteWebhookType.Normal + finalResponse.webhookType = VoteWebhookType.Rejected finalResponse.webhookMessage = "Vote rejected: A moderator has decided that this segment is correct" } } From b08f5c8390ff9af18d28f3d565a1ae4a9c57eb14 Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Sun, 13 Jun 2021 16:00:36 -0400 Subject: [PATCH 3/6] Don't break for incorrect votes --- src/routes/voteOnSponsorTime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 0aec481..756ebf2 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -426,7 +426,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) { //update the vote count on this sponsorTime //oldIncrementAmount will be zero is row is null - await db.prepare('run', 'UPDATE "sponsorTimes" SET ' + columnName + ' = ' + columnName + ' + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]); + await db.prepare('run', 'UPDATE "sponsorTimes" SET "' + columnName + '" = ' + columnName + ' + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]); if (isVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) { // Lock this submission await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 1 WHERE "UUID" = ?', [UUID]); From 588e0abdd8cf7b954d536dbc51feba6059f5a1f4 Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Sun, 13 Jun 2021 16:22:10 -0400 Subject: [PATCH 4/6] Fix type = 20 vote --- src/routes/voteOnSponsorTime.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 756ebf2..147139e 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -333,7 +333,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) { return res.status(403).send('Vote rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. Could you please send a message in Discord or Matrix so we can further help you?'); } - const voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect; + const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect; try { //check if vote has already happened @@ -426,7 +426,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) { //update the vote count on this sponsorTime //oldIncrementAmount will be zero is row is null - await db.prepare('run', 'UPDATE "sponsorTimes" SET "' + columnName + '" = ' + columnName + ' + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]); + await db.prepare('run', 'UPDATE "sponsorTimes" SET "' + columnName + '" = "' + columnName + '" + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]); if (isVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) { // Lock this submission await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 1 WHERE "UUID" = ?', [UUID]); From 17eb9604e70e0c1208b6b79e5ac71b18c54a4fdf Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Sun, 13 Jun 2021 17:13:44 -0400 Subject: [PATCH 5/6] Add banned username --- src/routes/setUsername.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/setUsername.ts b/src/routes/setUsername.ts index 0283f4d..73ff651 100644 --- a/src/routes/setUsername.ts +++ b/src/routes/setUsername.ts @@ -40,7 +40,7 @@ export async function setUsername(req: Request, res: Response) { userID = getHash(userID); } - if (["7e7eb6c6dbbdba6a106a38e87eae29ed8689d0033cb629bb324a8dab615c5a97", "e1839ce056d185f176f30a3d04a79242110fe46ad6e9bd1a9170f56857d1b148"].includes(userID)) { + if (["7e7eb6c6dbbdba6a106a38e87eae29ed8689d0033cb629bb324a8dab615c5a97", "e1839ce056d185f176f30a3d04a79242110fe46ad6e9bd1a9170f56857d1b148", "c3424f0d1f99631e6b36e5bf634af953e96b790705abd86a9c5eb312239cb765"].includes(userID)) { // Don't allow res.sendStatus(200); return; From e06eb96fa7b7b08cc95cef9b2ebf7565a3ec36b7 Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Mon, 14 Jun 2021 16:23:39 -0400 Subject: [PATCH 6/6] Add ability to ban specific category --- src/routes/shadowBanUser.ts | 10 ++- test/cases/shadowBanUser.ts | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 test/cases/shadowBanUser.ts diff --git a/src/routes/shadowBanUser.ts b/src/routes/shadowBanUser.ts index 4ecfc45..17b9798 100644 --- a/src/routes/shadowBanUser.ts +++ b/src/routes/shadowBanUser.ts @@ -1,6 +1,7 @@ import {db, privateDB} from '../databases/databases'; import {getHash} from '../utils/getHash'; import {Request, Response} from 'express'; +import { config } from '../config'; export async function shadowBanUser(req: Request, res: Response) { const userID = req.query.userID as string; @@ -8,12 +9,15 @@ export async function shadowBanUser(req: Request, res: Response) { let adminUserIDInput = req.query.adminUserID as string; const enabled = req.query.enabled === undefined - ? false + ? true : req.query.enabled === 'true'; //if enabled is false and the old submissions should be made visible again const unHideOldSubmissions = req.query.unHideOldSubmissions !== "false"; + const categories: string[] = req.query.categories ? JSON.parse(req.query.categories as string) : config.categoryList; + categories.filter((category) => typeof category === "string" && !(/[^a-z|_|-]/.test(category))); + if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined)) { //invalid request res.sendStatus(400); @@ -42,7 +46,7 @@ export async function shadowBanUser(req: Request, res: Response) { //find all previous submissions and hide them if (unHideOldSubmissions) { - await db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 1 WHERE "userID" = ? + await db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 1 WHERE "userID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")}) AND NOT EXISTS ( SELECT "videoID", "category" FROM "lockCategories" WHERE "sponsorTimes"."videoID" = "lockCategories"."videoID" AND "sponsorTimes"."category" = "lockCategories"."category")`, [userID]); } @@ -61,7 +65,7 @@ export async function shadowBanUser(req: Request, res: Response) { await Promise.all(allSegments.filter((item: {uuid: string}) => { return segmentsToIgnore.indexOf(item) === -1; }).map((UUID: string) => { - return db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 0 WHERE "UUID" = ?`, [UUID]); + return db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 0 WHERE "UUID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID]); })); } } diff --git a/test/cases/shadowBanUser.ts b/test/cases/shadowBanUser.ts new file mode 100644 index 0000000..5d6af7e --- /dev/null +++ b/test/cases/shadowBanUser.ts @@ -0,0 +1,126 @@ +import fetch from 'node-fetch'; +import {db, privateDB} from '../../src/databases/databases'; +import {Done, getbaseURL} from '../utils'; +import {getHash} from '../../src/utils/getHash'; + +describe('shadowBanUser', () => { + before(() => { + let startOfQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "service", "videoDuration", "hidden", "shadowHidden", "hashedVideoID") VALUES'; + db.prepare("run", startOfQuery + "('testtesttest', 1, 11, 2, 0, 'shadow-1-uuid-0', 'shadowBanned', 0, 50, 'sponsor', 'YouTube', 100, 0, 0, '" + getHash('testtesttest', 1) + "')"); + db.prepare("run", startOfQuery + "('testtesttest2', 1, 11, 2, 0, 'shadow-1-uuid-0-1', 'shadowBanned', 0, 50, 'sponsor', 'PeerTube', 120, 0, 0, '" + getHash('testtesttest2', 1) + "')"); + db.prepare("run", startOfQuery + "('testtesttest', 20, 33, 2, 0, 'shadow-1-uuid-2', 'shadowBanned', 0, 50, 'intro', 'YouTube', 101, 0, 0, '" + getHash('testtesttest', 1) + "')"); + + db.prepare("run", startOfQuery + "('testtesttest', 1, 11, 2, 0, 'shadow-2-uuid-0', 'shadowBanned2', 0, 50, 'sponsor', 'YouTube', 100, 0, 0, '" + getHash('testtesttest', 1) + "')"); + db.prepare("run", startOfQuery + "('testtesttest2', 1, 11, 2, 0, 'shadow-2-uuid-0-1', 'shadowBanned2', 0, 50, 'sponsor', 'PeerTube', 120, 0, 0, '" + getHash('testtesttest2', 1) + "')"); + db.prepare("run", startOfQuery + "('testtesttest', 20, 33, 2, 0, 'shadow-2-uuid-2', 'shadowBanned2', 0, 50, 'intro', 'YouTube', 101, 0, 0, '" + getHash('testtesttest', 1) + "')"); + + db.prepare("run", startOfQuery + "('testtesttest', 1, 11, 2, 0, 'shadow-3-uuid-0', 'shadowBanned3', 0, 50, 'sponsor', 'YouTube', 100, 0, 1, '" + getHash('testtesttest', 1) + "')"); + db.prepare("run", startOfQuery + "('testtesttest2', 1, 11, 2, 0, 'shadow-3-uuid-0-1', 'shadowBanned3', 0, 50, 'sponsor', 'PeerTube', 120, 0, 1, '" + getHash('testtesttest2', 1) + "')"); + db.prepare("run", startOfQuery + "('testtesttest', 20, 33, 2, 0, 'shadow-3-uuid-2', 'shadowBanned3', 0, 50, 'intro', 'YouTube', 101, 0, 1, '" + getHash('testtesttest', 1) + "')"); + privateDB.prepare("run", `INSERT INTO "shadowBannedUsers" VALUES('shadowBanned3')`); + + db.prepare("run", `INSERT INTO "vipUsers" ("userID") VALUES ('` + getHash("shadow-ban-vip") + "')"); + }); + + + it('Should be able to ban user and hide submissions', (done: Done) => { + fetch(getbaseURL() + "/api/shadowBanUser?userID=shadowBanned&adminUserID=shadow-ban-vip", { + method: 'POST' + }) + .then(async res => { + if (res.status !== 200) done("Status code was: " + res.status); + else { + const videoRow = await db.prepare('all', `SELECT "shadowHidden" FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" = ?`, ["shadowBanned", 1]); + const shadowRow = await privateDB.prepare('get', `SELECT * FROM "shadowBannedUsers" WHERE "userID" = ?`, ["shadowBanned"]); + + if (shadowRow && videoRow?.length === 3) { + done(); + } else { + done("Ban failed " + JSON.stringify(videoRow) + " " + JSON.stringify(shadowRow)); + } + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + + it('Should be able to unban user without unhiding submissions', (done: Done) => { + fetch(getbaseURL() + "/api/shadowBanUser?userID=shadowBanned&adminUserID=shadow-ban-vip&enabled=false&unHideOldSubmissions=false", { + method: 'POST' + }) + .then(async res => { + if (res.status !== 200) done("Status code was: " + res.status); + else { + const videoRow = await db.prepare('all', `SELECT "shadowHidden" FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" = ?`, ["shadowBanned", 1]); + const shadowRow = await privateDB.prepare('get', `SELECT * FROM "shadowBannedUsers" WHERE "userID" = ?`, ["shadowBanned"]); + + if (!shadowRow && videoRow?.length === 3) { + done(); + } else { + done("Unban failed " + JSON.stringify(videoRow) + " " + JSON.stringify(shadowRow)); + } + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + + it('Should be able to ban user and hide submissions from only some categories', (done: Done) => { + fetch(getbaseURL() + '/api/shadowBanUser?userID=shadowBanned2&adminUserID=shadow-ban-vip&categories=["sponsor"]', { + method: 'POST' + }) + .then(async res => { + if (res.status !== 200) done("Status code was: " + res.status); + else { + const videoRow: {category: string, shadowHidden: number}[] = (await db.prepare('all', `SELECT "shadowHidden", "category" FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" = ?`, ["shadowBanned2", 1])); + const shadowRow = await privateDB.prepare('get', `SELECT * FROM "shadowBannedUsers" WHERE "userID" = ?`, ["shadowBanned2"]); + + if (shadowRow && 2 == videoRow?.length && 2 === videoRow?.filter((elem) => elem?.category === "sponsor")?.length) { + done(); + } else { + done("Ban failed " + JSON.stringify(videoRow) + " " + JSON.stringify(shadowRow)); + } + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + + it('Should be able to unban user and unhide submissions', (done: Done) => { + fetch(getbaseURL() + "/api/shadowBanUser?userID=shadowBanned2&adminUserID=shadow-ban-vip&enabled=false", { + method: 'POST' + }) + .then(async res => { + if (res.status !== 200) done("Status code was: " + res.status); + else { + const videoRow = await db.prepare('all', `SELECT "shadowHidden" FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" = ?`, ["shadowBanned2", 1]); + const shadowRow = await privateDB.prepare('get', `SELECT * FROM "shadowBannedUsers" WHERE "userID" = ?`, ["shadowBanned2"]); + + if (!shadowRow && videoRow?.length === 0) { + done(); + } else { + done("Unban failed " + JSON.stringify(videoRow) + " " + JSON.stringify(shadowRow)); + } + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + + it('Should be able to unban user and unhide some submissions', (done: Done) => { + fetch(getbaseURL() + `/api/shadowBanUser?userID=shadowBanned3&adminUserID=shadow-ban-vip&enabled=false&categories=["sponsor"]`, { + method: 'POST' + }) + .then(async res => { + if (res.status !== 200) done("Status code was: " + res.status); + else { + const videoRow = await db.prepare('all', `SELECT "shadowHidden", "category" FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" = ?`, ["shadowBanned3", 1]); + const shadowRow = await privateDB.prepare('get', `SELECT * FROM "shadowBannedUsers" WHERE "userID" = ?`, ["shadowBanned3"]); + + if (!shadowRow && videoRow?.length === 1 && videoRow[0]?.category === "intro") { + done(); + } else { + done("Unban failed " + JSON.stringify(videoRow) + " " + JSON.stringify(shadowRow)); + } + } + }) + .catch(err => done("Couldn't call endpoint")); + }); + +});