From a2f2cf9c0d937c9fadfbbc1944d3411e9c743e13 Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 3 Feb 2022 17:44:29 -0500 Subject: [PATCH] update lockCategories - migration to remove invalid locks - lockCategories poi_highlight is now actionType poi - deleteLockCategories now takes actionType - update postLockCategories response, serverside filtering for accepted categories - fix tests accordingly --- databases/_upgrade_sponsorTimes_31.sql | 25 ++++++++++ src/routes/deleteLockCategories.ts | 39 +++++---------- src/routes/postLockCategories.ts | 60 +++++++++++----------- src/routes/postSkipSegments.ts | 2 +- src/routes/voteOnSponsorTime.ts | 25 ++++++---- test/cases/lockCategoriesRecords.ts | 69 +++++--------------------- 6 files changed, 94 insertions(+), 126 deletions(-) create mode 100644 databases/_upgrade_sponsorTimes_31.sql diff --git a/databases/_upgrade_sponsorTimes_31.sql b/databases/_upgrade_sponsorTimes_31.sql new file mode 100644 index 0000000..3414c46 --- /dev/null +++ b/databases/_upgrade_sponsorTimes_31.sql @@ -0,0 +1,25 @@ +BEGIN TRANSACTION; + +/* START lockCategory migrations +no sponsor migrations +no selfpromo migrations */ + +/* exclusive_access migrations */ +DELETE FROM "lockCategories" WHERE "category" = 'exclusive_access' AND "actionType" != 'full'; +/* delete all full locks on categories without full */ +DELETE FROM "lockCategories" WHERE "actionType" = 'full' AND "category" in ('interaction', 'intro', 'outro', 'preview', 'filler', 'music_offtopic', 'poi_highlight'); +/* delete all non-skip music_offtopic locks */ +DELETE FROM "lockCategories" WHERE "category" = 'music_offtopic' AND "actionType" != 'skip'; +/* convert all poi_highlight to actionType poi */ +UPDATE "lockCategories" SET "actionType" = 'poi' WHERE "category" = 'poi_highlight' AND "actionType" = 'skip'; +/* delete all non-skip poi_highlight locks */ +DELETE FROM "lockCategories" WHERE "category" = 'poi_highlight' AND "actionType" != 'poi'; + +/* END lockCategory migrations */ + +/* delete all redundant userName entries */ +DELETE FROM "userNames" WHERE "userName" = "userID" AND "locked" = 0; + +UPDATE "config" SET value = 31 WHERE key = 'version'; + +COMMIT; \ No newline at end of file diff --git a/src/routes/deleteLockCategories.ts b/src/routes/deleteLockCategories.ts index 369ea52..1e001aa 100644 --- a/src/routes/deleteLockCategories.ts +++ b/src/routes/deleteLockCategories.ts @@ -2,9 +2,10 @@ import { Request, Response } from "express"; import { isUserVIP } from "../utils/isUserVIP"; import { getHashCache } from "../utils/getHashCache"; import { db } from "../databases/databases"; -import { Category, Service, VideoID } from "../types/segments.model"; +import { ActionType, Category, Service, VideoID } from "../types/segments.model"; import { UserID } from "../types/user.model"; import { getService } from "../utils/getService"; +import { config } from "../config"; interface DeleteLockCategoriesRequest extends Request { body: { @@ -12,6 +13,7 @@ interface DeleteLockCategoriesRequest extends Request { service: string; userID: UserID; videoID: VideoID; + actionTypes: ActionType[]; }; } @@ -22,7 +24,8 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ videoID, userID, categories, - service + service, + actionTypes } } = req; @@ -32,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ || !categories || !Array.isArray(categories) || categories.length === 0 + || actionTypes.length === 0 ) { return res.status(400).json({ message: "Bad Format", @@ -48,33 +52,14 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ }); } - await deleteLockCategories(videoID, categories, getService(service)); + await deleteLockCategories(videoID, categories, actionTypes, getService(service)); return res.status(200).json({ message: `Removed lock categories entries for video ${videoID}` }); } -/** - * - * @param videoID - * @param categories If null, will remove all - * @param service - */ -export async function deleteLockCategories(videoID: VideoID, categories: Category[], service: Service): Promise { - type DBEntry = { category: Category }; - const dbEntries = await db.prepare( - "all", - 'SELECT * FROM "lockCategories" WHERE "videoID" = ? AND "service" = ?', - [videoID, service] - ) as Array; - - const entries = dbEntries.filter( - ({ category }: DBEntry) => categories === null || categories.indexOf(category) !== -1); - - await Promise.all( - entries.map(({ category }: DBEntry) => db.prepare( - "run", - 'DELETE FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?', - [videoID, service, category] - )) - ); +export async function deleteLockCategories(videoID: VideoID, categories = config.categoryList, actionTypes = [ActionType.Skip, ActionType.Mute], service: Service): Promise { + const arrJoin = (arr: string[]): string => `'${arr.join(`','`)}'`; + const categoryString = arrJoin(categories); + const actionTypeString = arrJoin(actionTypes); + await db.prepare("run", `DELETE FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" IN (${categoryString}) AND "actionType" IN (${actionTypeString})`, [videoID, service]); } diff --git a/src/routes/postLockCategories.ts b/src/routes/postLockCategories.ts index 400acd3..f8ccb8d 100644 --- a/src/routes/postLockCategories.ts +++ b/src/routes/postLockCategories.ts @@ -5,6 +5,7 @@ import { db } from "../databases/databases"; import { Request, Response } from "express"; import { ActionType, Category, VideoIDHash } from "../types/segments.model"; import { getService } from "../utils/getService"; +import { config } from "../config"; export async function postLockCategories(req: Request, res: Response): Promise { // Collect user input data @@ -44,25 +45,18 @@ export async function postLockCategories(req: Request, res: Response): Promise lock.category === category && lock.actionType === actionType)) { - locksToApply.push({ - category, - actionType - }); - } else { - overwrittenLocks.push({ - category, - actionType - }); - } - } + + // push new/ existing locks + const validLocks = createLockArray(categories, actionTypes); + for (const { category, actionType } of validLocks) { + const targetArray = existingLocks.some((lock) => lock.category === category && lock.actionType === actionType) + ? overwrittenLocks + : locksToApply; + targetArray.push({ + category, actionType + }); } // calculate hash of videoID @@ -99,20 +93,26 @@ export async function postLockCategories(req: Request, res: Response): Promise locksToApply.some((lock) => category === lock.category)))] - : [...filteredCategories], // Legacy - submittedValues: [...locksToApply, ...overwrittenLocks], + submitted: deDupArray(validLocks.map(e => e.category)), + submittedValues: validLocks, }); } -function filterData(data: T[]): T[] { - // get user categories not already submitted that match accepted format - const filtered = data.filter((elem) => { - return !!elem.match(/^[_a-zA-Z]+$/); +const isValidCategoryActionPair = (category: Category, actionType: ActionType): boolean => + config.categorySupport?.[category]?.includes(actionType); + +// filter out any invalid category/action pairs +type validLockArray = { category: Category, actionType: ActionType }[]; +const createLockArray = (categories: Category[], actionTypes: ActionType[]): validLockArray => { + const validLocks: validLockArray = []; + categories.forEach(category => { + actionTypes.forEach(actionType => { + if (isValidCategoryActionPair(category, actionType)) { + validLocks.push({ category, actionType }); + } + }); }); - // remove any duplicates - return filtered.filter((elem, index) => { - return filtered.indexOf(elem) === index; - }); -} + return validLocks; +}; + +const deDupArray = (arr: any[]): any[] => [...new Set(arr)]; \ No newline at end of file diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index 568199d..d704c85 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -488,7 +488,7 @@ async function updateDataIfVideoDurationChange(videoID: VideoID, service: Servic await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]); } lockedCategoryList = []; - deleteLockCategories(videoID, null, service); + deleteLockCategories(videoID, null, null, service); } return { diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 4782508..63db152 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -106,8 +106,11 @@ async function checkVideoDuration(UUID: SegmentUUID) { if (videoDurationChanged(latestSubmission.videoDuration, apiVideoDuration)) { Logger.info(`Video duration changed for ${videoID} from ${latestSubmission.videoDuration} to ${apiVideoDuration}`); - await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE videoID = ? AND service = ? AND submissionTime < ?`, - [videoID, service, latestSubmission.submissionTime]); + await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 + WHERE videoID = ? AND service = ? AND submissionTime < ? + hidden" = 0 AND "shadowHidden" = 0 AND + "actionType" != 'full' AND "votes" > -2`, + [videoID, service, latestSubmission.submissionTime]); } } @@ -231,13 +234,12 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i if (segmentInfo.actionType === ActionType.Full) { return { status: 400, message: "Not allowed to change category of a full video segment" }; } - - if (!config.categoryList.includes(category)) { - return { status: 400, message: "Category doesn't exist." }; - } if (segmentInfo.actionType === ActionType.Poi) { return { status: 400, message: "Not allowed to change category for single point segments" }; } + if (!config.categoryList.includes(category)) { + return { status: 400, message: "Category doesn't exist." }; + } // Ignore vote if the next category is locked const nextCategoryLocked = await db.prepare("get", `SELECT "videoID", "category" FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?`, [segmentInfo.videoID, segmentInfo.service, category]); @@ -417,6 +419,12 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect; + // no restrictions on checkDuration + // check duration of all submissions on this video + if (type < 0) { + checkVideoDuration(UUID); + } + try { // check if vote has already happened const votesRow = await privateDB.prepare("get", `SELECT "type" FROM "votes" WHERE "userID" = ? AND "UUID" = ?`, [userID, UUID]); @@ -496,11 +504,6 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID // update the vote count on this sponsorTime await db.prepare("run", `UPDATE "sponsorTimes" SET "votes" = "votes" + ? WHERE "UUID" = ?`, [incrementAmount - oldIncrementAmount, UUID]); - // check duration of all submissions on this video - if (type < 0) { - checkVideoDuration(UUID); - } - // additional procesing for VIP // on VIP upvote if (isVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) { diff --git a/test/cases/lockCategoriesRecords.ts b/test/cases/lockCategoriesRecords.ts index dbc4969..67c793f 100644 --- a/test/cases/lockCategoriesRecords.ts +++ b/test/cases/lockCategoriesRecords.ts @@ -76,7 +76,7 @@ describe("lockCategoriesRecords", () => { const expected = { submitted: [ "outro", - "shilling", + "intro" ], submittedValues: [ { @@ -87,14 +87,6 @@ describe("lockCategoriesRecords", () => { actionType: "mute", category: "outro" }, - { - actionType: "skip", - category: "shilling" - }, - { - actionType: "mute", - category: "shilling" - }, { actionType: "skip", category: "intro" @@ -132,14 +124,14 @@ describe("lockCategoriesRecords", () => { .then(async res => { assert.strictEqual(res.status, 200); const result = await checkLockCategories(videoID); - assert.strictEqual(result.length, 8); + assert.strictEqual(result.length, 6); const oldRecordNotChangeReason = result.filter(item => item.reason === "reason-2" && ["sponsor", "intro"].includes(item.category) ); const newRecordWithEmptyReason = result.filter(item => - item.reason === "" && ["outro", "shilling"].includes(item.category) + item.reason === "" && ["outro"].includes(item.category) ); - assert.strictEqual(newRecordWithEmptyReason.length, 4); + assert.strictEqual(newRecordWithEmptyReason.length, 2); assert.strictEqual(oldRecordNotChangeReason.length, 4); done(); }) @@ -164,7 +156,6 @@ describe("lockCategoriesRecords", () => { const expected = { submitted: [ "outro", - "shilling", "intro" ], }; @@ -196,7 +187,6 @@ describe("lockCategoriesRecords", () => { const expectedWithNewReason = [ "outro", - "shilling", "intro" ]; @@ -204,7 +194,7 @@ describe("lockCategoriesRecords", () => { .then(async res => { assert.strictEqual(res.status, 200); const result = await checkLockCategories(videoID); - assert.strictEqual(result.length, 8); + assert.strictEqual(result.length, 6); const newRecordWithNewReason = result.filter(item => expectedWithNewReason.includes(item.category) && item.reason === "new reason" ); @@ -212,7 +202,7 @@ describe("lockCategoriesRecords", () => { item.reason === "reason-2" ); - assert.strictEqual(newRecordWithNewReason.length, 6); + assert.strictEqual(newRecordWithNewReason.length, 4); assert.strictEqual(oldRecordNotChangeReason.length, 2); done(); }) @@ -220,56 +210,20 @@ describe("lockCategoriesRecords", () => { }); it("Should be able to submit categories with _ in the category", (done) => { - const json = { - videoID: "underscore", - userID: lockVIPUser, - categories: [ - "word_word", - ], - }; - client.post(endpoint, json) - .then(async res => { - assert.strictEqual(res.status, 200); - const result = await checkLockCategories("underscore"); - assert.strictEqual(result.length, 2); - done(); - }) - .catch(err => done(err)); - }); - - it("Should be able to submit categories with upper and lower case in the category", (done) => { - const json = { - videoID: "bothCases", - userID: lockVIPUser, - categories: [ - "wordWord", - ], - }; - client.post(endpoint, json) - .then(async res => { - assert.strictEqual(res.status, 200); - const result = await checkLockCategories("bothCases"); - assert.strictEqual(result.length, 2); - done(); - }) - .catch(err => done(err)); - }); - - it("Should not be able to submit categories with $ in the category", (done) => { - const videoID = "specialChar"; + const videoID = "underscore"; const json = { videoID, userID: lockVIPUser, categories: [ - "word&word", + "exclusive_access", ], + actionTypes: ["full"] }; - client.post(endpoint, json) .then(async res => { assert.strictEqual(res.status, 200); const result = await checkLockCategories(videoID); - assert.strictEqual(result.length, 0); + assert.strictEqual(result.length, 1); done(); }) .catch(err => done(err)); @@ -418,6 +372,7 @@ describe("lockCategoriesRecords", () => { categories: [ "sponsor", ], + actionTypes: ["skip", "mute"] }; client.delete(endpoint, { data: json }) @@ -438,6 +393,7 @@ describe("lockCategoriesRecords", () => { categories: [ "sponsor", ], + actionTypes: ["skip", "mute"] }; client.delete(endpoint, { data: json }) @@ -531,7 +487,6 @@ describe("lockCategoriesRecords", () => { "sponsor", "intro", "outro", - "shilling" ], }; client.get(endpoint, { params: { videoID: "no-segments-video-id" } })