diff --git a/DatabaseSchema.md b/DatabaseSchema.md index 957edb1..ab11b53 100644 --- a/DatabaseSchema.md +++ b/DatabaseSchema.md @@ -209,6 +209,7 @@ | userID | TEXT | not null | | hashedIP | TEXT | not null | | type | INTEGER | not null | +| originalVoteType | INTEGER | not null | # Since type was reused to also specify the number of votes removed when less than 0, this is being used for the actual type | index | field | | -- | :--: | diff --git a/databases/_upgrade_private_10.sql b/databases/_upgrade_private_10.sql new file mode 100644 index 0000000..bdac3b6 --- /dev/null +++ b/databases/_upgrade_private_10.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +-- Add primary keys + +ALTER TABLE "votes" ADD "originalType" INTEGER NOT NULL default -1; + +UPDATE "config" SET value = 10 WHERE key = 'version'; + +COMMIT; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 2efd0c9..1a62374 100644 --- a/src/config.ts +++ b/src/config.ts @@ -42,6 +42,7 @@ addDefaults(config, { discordNeuralBlockRejectWebhookURL: null, discordFailedReportChannelWebhookURL: null, discordReportChannelWebhookURL: null, + discordMaliciousReportWebhookURL: null, getTopUsersCacheTimeMinutes: 240, globalSalt: null, mode: "", diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 1412a24..002b63b 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -11,7 +11,7 @@ import { getIP } from "../utils/getIP"; import { getHashCache } from "../utils/getHashCache"; import { config } from "../config"; import { UserID } from "../types/user.model"; -import { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, VideoDuration, ActionType } from "../types/segments.model"; +import { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, VideoDuration, ActionType, VoteType } from "../types/segments.model"; import { QueryCacher } from "../utils/queryCacher"; import axios from "axios"; @@ -36,6 +36,7 @@ interface FinalResponse { interface VoteData { UUID: string; nonAnonUserID: string; + originalType: VoteType; voteTypeEnum: number; isTempVIP: boolean; isVIP: boolean; @@ -112,7 +113,9 @@ async function sendWebhooks(voteData: VoteData) { if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) { let webhookURL: string = null; - if (voteData.voteTypeEnum === voteTypes.normal) { + if (voteData.originalType === VoteType.Malicious) { + webhookURL = config.discordMaliciousReportWebhookURL; + } else if (voteData.voteTypeEnum === voteTypes.normal) { switch (voteData.finalResponse.webhookType) { case VoteWebhookType.Normal: webhookURL = config.discordReportChannelWebhookURL; @@ -329,6 +332,8 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID return { status: 200 }; } + const originalType = type; + //hash the userID const nonAnonUserID = await getHashCache(paramUserID); const userID = await getHashCache(paramUserID + UUID); @@ -421,13 +426,13 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID let incrementAmount = 0; let oldIncrementAmount = 0; - if (type == 1) { + if (type == VoteType.Upvote) { //upvote incrementAmount = 1; - } else if (type == 0) { + } else if (type === VoteType.Downvote || type === VoteType.Malicious) { //downvote incrementAmount = -1; - } else if (type == 20) { + } else if (type == VoteType.Undo) { //undo/cancel vote incrementAmount = 0; } else { @@ -435,17 +440,13 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID return { status: 400 }; } if (votesRow) { - if (votesRow.type === 1) { - //upvote + if (votesRow.type === VoteType.Upvote) { oldIncrementAmount = 1; - } else if (votesRow.type === 0) { - //downvote + } else if (votesRow.type === VoteType.Downvote) { oldIncrementAmount = -1; - } else if (votesRow.type === 2) { - //extra downvote + } else if (votesRow.type === VoteType.ExtraDownvote) { oldIncrementAmount = -4; - } else if (votesRow.type === 20) { - //undo/cancel vote + } else if (votesRow.type === VoteType.Undo) { oldIncrementAmount = 0; } else if (votesRow.type < 0) { //vip downvote @@ -466,8 +467,14 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID type = incrementAmount; } + if (type === VoteType.Malicious) { + incrementAmount = -Math.min(segmentInfo.votes + 2 - oldIncrementAmount, 5); + type = incrementAmount; + } + // Only change the database if they have made a submission before and haven't voted recently const userAbleToVote = (!(isOwnSubmission && incrementAmount > 0 && oldIncrementAmount >= 0) + && !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter) && !finalResponse.blockVote && finalResponse.finalStatus === 200 && (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined @@ -480,9 +487,9 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID if (ableToVote) { //update the votes table if (votesRow) { - await privateDB.prepare("run", `UPDATE "votes" SET "type" = ? WHERE "userID" = ? AND "UUID" = ?`, [type, userID, UUID]); + await privateDB.prepare("run", `UPDATE "votes" SET "type" = ?, "originalType" = ? WHERE "userID" = ? AND "UUID" = ?`, [type, originalType, userID, UUID]); } else { - await privateDB.prepare("run", `INSERT INTO "votes" VALUES(?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, type, nonAnonUserID]); + await privateDB.prepare("run", `INSERT INTO "votes" VALUES(?, ?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, type, nonAnonUserID, originalType]); } // update the vote count on this sponsorTime @@ -510,6 +517,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID sendWebhooks({ UUID, nonAnonUserID, + originalType, voteTypeEnum, isTempVIP, isVIP, diff --git a/src/types/config.model.ts b/src/types/config.model.ts index 774bd01..a9f2532 100644 --- a/src/types/config.model.ts +++ b/src/types/config.model.ts @@ -24,6 +24,7 @@ export interface SBSConfig { discordFailedReportChannelWebhookURL?: string; discordFirstTimeSubmissionsWebhookURL?: string; discordCompletelyIncorrectReportWebhookURL?: string; + discordMaliciousReportWebhookURL?: string; neuralBlockURL?: string; discordNeuralBlockRejectWebhookURL?: string; userCounterURL?: string; diff --git a/src/types/segments.model.ts b/src/types/segments.model.ts index 5edb3e0..f657d6c 100644 --- a/src/types/segments.model.ts +++ b/src/types/segments.model.ts @@ -120,3 +120,12 @@ export enum SortableFields { votes = "votes", views = "views", } + + +export enum VoteType { + Downvote = 0, + Upvote = 1, + ExtraDownvote = 2, + Undo = 20, + Malicious = 30 +} \ No newline at end of file diff --git a/test/cases/voteOnSponsorTime.ts b/test/cases/voteOnSponsorTime.ts index 9fad1db..ebaf2eb 100644 --- a/test/cases/voteOnSponsorTime.ts +++ b/test/cases/voteOnSponsorTime.ts @@ -67,6 +67,8 @@ describe("voteOnSponsorTime", () => { await db.prepare("run", insertSponsorTimeQuery, ["duration-changed", 1, 12, 0, 0, "duration-changed-uuid-3", "testman", 20, 0, "sponsor", "skip", 0, 0]); // add videoDuration to duration-changed-uuid-2 await db.prepare("run", `UPDATE "sponsorTimes" SET "videoDuration" = 150 WHERE "UUID" = 'duration-changed-uuid-2'`); + await db.prepare("run", insertSponsorTimeQuery, ["chapter-video", 1, 10, 0, 0, "chapter-uuid-1", "testman", 0, 0, "chapter", "chapter", 0, 0]); + await db.prepare("run", insertSponsorTimeQuery, ["chapter-video", 1, 10, 0, 0, "non-chapter-uuid-2", "testman", 0, 0, "sponsor", "skip", 0, 0]); const insertWarningQuery = 'INSERT INTO "warnings" ("userID", "issueTime", "issuerUserID", "enabled") VALUES(?, ?, ?, ?)'; await db.prepare("run", insertWarningQuery, [warnUser01Hash, now, warnVip01Hash, 1]); @@ -223,6 +225,30 @@ describe("voteOnSponsorTime", () => { .catch(err => done(err)); }); + it("should be able to completely downvote chapter using malicious", (done) => { + const UUID = "chapter-uuid-1"; + postVote(randomID2, UUID, 30) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegmentVotes(UUID); + assert.strictEqual(row.votes, -2); + done(); + }) + .catch(err => done(err)); + }); + + it("should not be able to completely downvote non-chapter using malicious", (done) => { + const UUID = "non-chapter-uuid-2"; + postVote(randomID2, UUID, 30) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegmentVotes(UUID); + assert.strictEqual(row.votes, 0); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to vote for a category and it should add your vote to the database", (done) => { const UUID = "vote-uuid-4"; postVoteCategory(randomID2, UUID, "intro")