mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-06 19:47:00 +03:00
Merge pull request #440 from ajayyy/full-video-labels
Full video labeling
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
| -- | :--: | -- |
|
||||
| videoID | TEXT | not null |
|
||||
| userID | TEXT | not null |
|
||||
| actionType | TEXT | not null, default 'skip' |
|
||||
| category | TEXT | not null |
|
||||
| hashedVideoID | TEXT | not null, default '' |
|
||||
| reason | TEXT | not null, default '' |
|
||||
|
||||
21
databases/_upgrade_sponsorTimes_29.sql
Normal file
21
databases/_upgrade_sponsorTimes_29.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "sqlb_temp_table_29" (
|
||||
"videoID" TEXT NOT NULL,
|
||||
"userID" TEXT NOT NULL,
|
||||
"actionType" TEXT NOT NULL DEFAULT 'skip',
|
||||
"category" TEXT NOT NULL,
|
||||
"hashedVideoID" TEXT NOT NULL default '',
|
||||
"reason" TEXT NOT NULL default '',
|
||||
"service" TEXT NOT NULL default 'YouTube'
|
||||
);
|
||||
|
||||
INSERT INTO sqlb_temp_table_29 SELECT "videoID","userID",'skip',"category","hashedVideoID","reason","service" FROM "lockCategories";
|
||||
INSERT INTO sqlb_temp_table_29 SELECT "videoID","userID",'mute',"category","hashedVideoID","reason","service" FROM "lockCategories";
|
||||
|
||||
DROP TABLE "lockCategories";
|
||||
ALTER TABLE sqlb_temp_table_29 RENAME TO "lockCategories";
|
||||
|
||||
UPDATE "config" SET value = 29 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
@@ -21,8 +21,8 @@ addDefaults(config, {
|
||||
webhooks: [],
|
||||
categoryList: ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler", "poi_highlight", "chapter"],
|
||||
categorySupport: {
|
||||
sponsor: ["skip", "mute"],
|
||||
selfpromo: ["skip", "mute"],
|
||||
sponsor: ["skip", "mute", "full"],
|
||||
selfpromo: ["skip", "mute", "full"],
|
||||
interaction: ["skip", "mute"],
|
||||
intro: ["skip", "mute"],
|
||||
outro: ["skip", "mute"],
|
||||
|
||||
@@ -167,7 +167,7 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P
|
||||
// amountOfChoices specifies the maximum amount of choices to return, 1 or more.
|
||||
// Choices are unique
|
||||
// If a predicate is given, it will only filter choices following it, and will leave the rest in the list
|
||||
function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOfChoices: number, predicate?: (choice: T) => void): T[] {
|
||||
function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOfChoices: number, filterLocked = false, predicate?: (choice: T) => void): T[] {
|
||||
//trivial case: no need to go through the whole process
|
||||
if (amountOfChoices >= choices.length) {
|
||||
return choices;
|
||||
@@ -183,6 +183,10 @@ function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOf
|
||||
const splitArray = partition(choices, predicate);
|
||||
filteredChoices = splitArray[0];
|
||||
forceIncludedChoices = splitArray[1];
|
||||
|
||||
if (filterLocked && filteredChoices.some((value) => value.locked)) {
|
||||
filteredChoices = filteredChoices.filter((value) => value.locked);
|
||||
}
|
||||
}
|
||||
|
||||
//assign a weight to each choice
|
||||
@@ -200,7 +204,7 @@ function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOf
|
||||
|
||||
// Nothing to filter for
|
||||
if (amountOfChoices >= choicesWithWeights.length) {
|
||||
return choices;
|
||||
return [...forceIncludedChoices, ...filteredChoices];
|
||||
}
|
||||
|
||||
//iterate and find amountOfChoices choices
|
||||
@@ -230,11 +234,12 @@ async function chooseSegments(videoID: VideoID, service: Service, segments: DBSe
|
||||
? await QueryCacher.get(fetchData, skipSegmentGroupsKey(videoID, service))
|
||||
: await fetchData();
|
||||
|
||||
// Filter for only 1 item for POI categories
|
||||
return getWeightedRandomChoice(groups, 1, (choice) => getCategoryActionType(choice.segments[0].category) === CategoryActionType.POI)
|
||||
.map(//randomly choose 1 good segment per group and return them
|
||||
group => getWeightedRandomChoice(group.segments, 1)[0]
|
||||
);
|
||||
// Filter for only 1 item for POI categories and Full video
|
||||
let chosenGroups = getWeightedRandomChoice(groups, 1, true, (choice) => choice.segments[0].actionType === ActionType.Full);
|
||||
chosenGroups = getWeightedRandomChoice(chosenGroups, 1, true, (choice) => getCategoryActionType(choice.segments[0].category) === CategoryActionType.POI);
|
||||
return chosenGroups.map(//randomly choose 1 good segment per group and return them
|
||||
group => getWeightedRandomChoice(group.segments, 1)[0]
|
||||
);
|
||||
}
|
||||
|
||||
//This function will find segments that are contained inside of eachother, called similar segments
|
||||
@@ -300,7 +305,7 @@ function splitPercentOverlap(groups: OverlappingSegmentGroup[]): OverlappingSegm
|
||||
group.segments.forEach((segment) => {
|
||||
const bestGroup = result.find((group) => {
|
||||
// At least one segment in the group must have high % overlap or the same action type
|
||||
// Since POI segments will always have 0 overlap, they will always be in their own groups
|
||||
// Since POI and Full video segments will always have <= 0 overlap, they will always be in their own groups
|
||||
return group.segments.some((compareSegment) => {
|
||||
const overlap = Math.min(segment.endTime, compareSegment.endTime) - Math.max(segment.startTime, compareSegment.startTime);
|
||||
const overallDuration = Math.max(segment.endTime, compareSegment.endTime) - Math.min(segment.startTime, compareSegment.startTime);
|
||||
|
||||
@@ -3,14 +3,15 @@ import { getHashCache } from "../utils/getHashCache";
|
||||
import { isUserVIP } from "../utils/isUserVIP";
|
||||
import { db } from "../databases/databases";
|
||||
import { Request, Response } from "express";
|
||||
import { VideoIDHash } from "../types/segments.model";
|
||||
import { ActionType, Category, VideoIDHash } from "../types/segments.model";
|
||||
import { getService } from "../utils/getService";
|
||||
|
||||
export async function postLockCategories(req: Request, res: Response): Promise<string[]> {
|
||||
// Collect user input data
|
||||
const videoID = req.body.videoID;
|
||||
let userID = req.body.userID;
|
||||
const categories = req.body.categories;
|
||||
const categories = req.body.categories as Category[];
|
||||
const actionTypes = req.body.actionTypes as ActionType[] || [ActionType.Skip, ActionType.Mute];
|
||||
const reason: string = req.body.reason ?? "";
|
||||
const service = getService(req.body.service);
|
||||
|
||||
@@ -20,6 +21,8 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
|| !categories
|
||||
|| !Array.isArray(categories)
|
||||
|| categories.length === 0
|
||||
|| !Array.isArray(actionTypes)
|
||||
|| actionTypes.length === 0
|
||||
) {
|
||||
res.status(400).json({
|
||||
message: "Bad Format",
|
||||
@@ -38,38 +41,39 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing lock categories markers
|
||||
let noCategoryList = await db.prepare("all", 'SELECT "category" from "lockCategories" where "videoID" = ? AND "service" = ?', [videoID, service]);
|
||||
if (!noCategoryList || noCategoryList.length === 0) {
|
||||
noCategoryList = [];
|
||||
} else {
|
||||
noCategoryList = noCategoryList.map((obj: any) => {
|
||||
return obj.category;
|
||||
});
|
||||
const existingLocks = (await db.prepare("all", 'SELECT "category", "actionType" from "lockCategories" where "videoID" = ? AND "service" = ?', [videoID, service])) as
|
||||
{ category: Category, actionType: ActionType }[];
|
||||
|
||||
const filteredCategories = filterData(categories);
|
||||
const filteredActionTypes = filterData(actionTypes);
|
||||
|
||||
const locksToApply: { category: Category, actionType: ActionType }[] = [];
|
||||
const overwrittenLocks: { category: Category, actionType: ActionType }[] = [];
|
||||
for (const category of filteredCategories) {
|
||||
for (const actionType of filteredActionTypes) {
|
||||
if (!existingLocks.some((lock) => lock.category === category && lock.actionType === actionType)) {
|
||||
locksToApply.push({
|
||||
category,
|
||||
actionType
|
||||
});
|
||||
} else {
|
||||
overwrittenLocks.push({
|
||||
category,
|
||||
actionType
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get user categories not already submitted that match accepted format
|
||||
let filteredCategories = categories.filter((category) => {
|
||||
return !!category.match(/^[_a-zA-Z]+$/);
|
||||
});
|
||||
// remove any duplicates
|
||||
filteredCategories = filteredCategories.filter((category, index) => {
|
||||
return filteredCategories.indexOf(category) === index;
|
||||
});
|
||||
|
||||
const categoriesToMark = filteredCategories.filter((category) => {
|
||||
return noCategoryList.indexOf(category) === -1;
|
||||
});
|
||||
|
||||
// calculate hash of videoID
|
||||
const hashedVideoID: VideoIDHash = await getHashCache(videoID, 1);
|
||||
|
||||
// create database entry
|
||||
for (const category of categoriesToMark) {
|
||||
for (const lock of locksToApply) {
|
||||
try {
|
||||
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "category", "hashedVideoID", "reason", "service") VALUES(?, ?, ?, ?, ?, ?)`, [videoID, userID, category, hashedVideoID, reason, service]);
|
||||
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "actionType", "category", "hashedVideoID", "reason", "service") VALUES(?, ?, ?, ?, ?, ?, ?)`, [videoID, userID, lock.actionType, lock.category, hashedVideoID, reason, service]);
|
||||
} catch (err) {
|
||||
Logger.error(`Error submitting 'lockCategories' marker for category '${category}' for video '${videoID}' (${service})`);
|
||||
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
|
||||
Logger.error(err as string);
|
||||
res.status(500).json({
|
||||
message: "Internal Server Error: Could not write marker to the database.",
|
||||
@@ -78,19 +82,14 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
}
|
||||
|
||||
// update reason for existed categories
|
||||
let overlapCategories = [];
|
||||
if (reason.length !== 0) {
|
||||
overlapCategories = filteredCategories.filter((category) => {
|
||||
return noCategoryList.indexOf(category) !== -1;
|
||||
});
|
||||
|
||||
for (const category of overlapCategories) {
|
||||
for (const lock of overwrittenLocks) {
|
||||
try {
|
||||
await db.prepare("run",
|
||||
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "category" = ? AND "service" = ?',
|
||||
[reason, userID, videoID, category, service]);
|
||||
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "actionType" = ? AND "category" = ? AND "service" = ?',
|
||||
[reason, userID, videoID, lock.actionType, lock.category, service]);
|
||||
} catch (err) {
|
||||
Logger.error(`Error submitting 'lockCategories' marker for category '${category}' for video '${videoID} (${service})'`);
|
||||
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
|
||||
Logger.error(err as string);
|
||||
res.status(500).json({
|
||||
message: "Internal Server Error: Could not write marker to the database.",
|
||||
@@ -100,6 +99,20 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
submitted: [...categoriesToMark, ...overlapCategories],
|
||||
submitted: reason.length === 0
|
||||
? [...filteredCategories.filter(((category) => locksToApply.some((lock) => category === lock.category)))]
|
||||
: [...filteredCategories], // Legacy
|
||||
submittedValues: [...locksToApply, ...overwrittenLocks],
|
||||
});
|
||||
}
|
||||
|
||||
function filterData<T extends string>(data: T[]): T[] {
|
||||
// get user categories not already submitted that match accepted format
|
||||
const filtered = data.filter((elem) => {
|
||||
return !!elem.match(/^[_a-zA-Z]+$/);
|
||||
});
|
||||
// remove any duplicates
|
||||
return filtered.filter((elem, index) => {
|
||||
return filtered.indexOf(elem) === index;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,17 +9,18 @@ import { getIP } from "../utils/getIP";
|
||||
import { getFormattedTime } from "../utils/getFormattedTime";
|
||||
import { dispatchEvent } from "../utils/webhookUtils";
|
||||
import { Request, Response } from "express";
|
||||
import { ActionType, Category, CategoryActionType, IncomingSegment, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model";
|
||||
import { ActionType, Category, CategoryActionType, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model";
|
||||
import { deleteLockCategories } from "./deleteLockCategories";
|
||||
import { getCategoryActionType } from "../utils/categoryInfo";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import { getReputation } from "../utils/reputation";
|
||||
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { isUserVIP } from "../utils/isUserVIP";
|
||||
import { parseUserAgent } from "../utils/userAgent";
|
||||
import { getService } from "../utils/getService";
|
||||
import axios from "axios";
|
||||
import { vote } from "./voteOnSponsorTime";
|
||||
|
||||
type CheckResult = {
|
||||
pass: boolean,
|
||||
@@ -238,7 +239,7 @@ async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
|
||||
const startTime = parseFloat(segments[i].segment[0]);
|
||||
const endTime = parseFloat(segments[i].segment[1]);
|
||||
|
||||
const UUID = getSubmissionUUID(submission.videoID, segments[i].actionType, submission.userID, startTime, endTime, submission.service);
|
||||
const UUID = getSubmissionUUID(submission.videoID, segments[i].category, segments[i].actionType, submission.userID, startTime, endTime, submission.service);
|
||||
// Send to Discord
|
||||
// Note, if this is too spammy. Consider sending all the segments as one Webhook
|
||||
sendWebhooksNB(submission.userID, submission.videoID, UUID, startTime, endTime, segments[i].category, nbPredictions.probabilities[predictionIdx], data);
|
||||
@@ -343,7 +344,7 @@ function checkInvalidFields(videoID: VideoID, userID: UserID, segments: Incoming
|
||||
return CHECK_PASS;
|
||||
}
|
||||
|
||||
async function checkEachSegmentValid(userID: string, videoID: VideoID,
|
||||
async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, userID: HashedUserID, videoID: VideoID,
|
||||
segments: IncomingSegment[], service: string, isVIP: boolean, lockedCategoryList: Array<any>): Promise<CheckResult> {
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
@@ -357,7 +358,7 @@ async function checkEachSegmentValid(userID: string, videoID: VideoID,
|
||||
}
|
||||
|
||||
// Reject segment if it's in the locked categories list
|
||||
const lockIndex = lockedCategoryList.findIndex(c => segments[i].category === c.category);
|
||||
const lockIndex = lockedCategoryList.findIndex(c => segments[i].category === c.category && segments[i].actionType === c.actionType);
|
||||
if (!isVIP && lockIndex !== -1) {
|
||||
// TODO: Do something about the fradulent submission
|
||||
Logger.warn(`Caught a submission for a locked category. userID: '${userID}', videoID: '${videoID}', category: '${segments[i].category}', times: ${segments[i].segment}`);
|
||||
@@ -383,8 +384,10 @@ async function checkEachSegmentValid(userID: string, videoID: VideoID,
|
||||
|
||||
if (isNaN(startTime) || isNaN(endTime)
|
||||
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime > endTime
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.Skippable && startTime === endTime)
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.POI && startTime !== endTime)) {
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.Skippable
|
||||
&& segments[i].actionType !== ActionType.Full && startTime === endTime)
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.POI && startTime !== endTime)
|
||||
|| (segments[i].actionType === ActionType.Full && (startTime !== 0 || endTime !== 0))) {
|
||||
//invalid request
|
||||
return { pass: false, errorMessage: "One of your segments times are invalid (too short, startTime before endTime, etc.)", errorCode: 400 };
|
||||
}
|
||||
@@ -394,16 +397,24 @@ async function checkEachSegmentValid(userID: string, videoID: VideoID,
|
||||
return { pass: false, errorMessage: `POI cannot be that early`, errorCode: 400 };
|
||||
}
|
||||
|
||||
if (!isVIP && segments[i].category === "sponsor" && Math.abs(startTime - endTime) < 1) {
|
||||
if (!isVIP && segments[i].category === "sponsor"
|
||||
&& segments[i].actionType !== ActionType.Full && Math.abs(startTime - endTime) < 1) {
|
||||
// Too short
|
||||
return { pass: false, errorMessage: "Sponsors must be longer than 1 second long", errorCode: 400 };
|
||||
return { pass: false, errorMessage: "Segments must be longer than 1 second long", errorCode: 400 };
|
||||
}
|
||||
|
||||
//check if this info has already been submitted before
|
||||
const duplicateCheck2Row = await db.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "startTime" = ?
|
||||
const duplicateCheck2Row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "startTime" = ?
|
||||
and "endTime" = ? and "category" = ? and "actionType" = ? and "videoID" = ? and "service" = ?`, [startTime, endTime, segments[i].category, segments[i].actionType, videoID, service]);
|
||||
if (duplicateCheck2Row.count > 0) {
|
||||
return { pass: false, errorMessage: "Sponsors has already been submitted before.", errorCode: 409 };
|
||||
if (duplicateCheck2Row) {
|
||||
if (segments[i].actionType === ActionType.Full) {
|
||||
// Forward as vote
|
||||
await vote(rawIP, duplicateCheck2Row.UUID, paramUserID, 1);
|
||||
segments[i].ignoreSegment = true;
|
||||
continue;
|
||||
} else {
|
||||
return { pass: false, errorMessage: "Segment has already been submitted before.", errorCode: 409 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,13 +450,14 @@ async function checkByAutoModerator(videoID: any, userID: any, segments: Array<a
|
||||
}
|
||||
|
||||
async function updateDataIfVideoDurationChange(videoID: VideoID, service: Service, videoDuration: VideoDuration, videoDurationParam: VideoDuration) {
|
||||
let lockedCategoryList = await db.prepare("all", 'SELECT category, reason from "lockCategories" where "videoID" = ? AND "service" = ?', [videoID, service]);
|
||||
let lockedCategoryList = await db.prepare("all", 'SELECT category, "actionType", reason from "lockCategories" where "videoID" = ? AND "service" = ?', [videoID, service]);
|
||||
|
||||
const previousSubmissions = await db.prepare("all",
|
||||
`SELECT "videoDuration", "UUID"
|
||||
FROM "sponsorTimes"
|
||||
WHERE "videoID" = ? AND "service" = ? AND
|
||||
"hidden" = 0 AND "shadowHidden" = 0 AND
|
||||
"actionType" != 'full' AND
|
||||
"votes" > -2 AND "videoDuration" != 0`,
|
||||
[videoID, service]
|
||||
) as {videoDuration: VideoDuration, UUID: SegmentUUID}[];
|
||||
@@ -573,15 +585,15 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { videoID, userID, service, videoDuration, videoDurationParam, segments, userAgent } = preprocessInput(req);
|
||||
let { videoID, userID: paramUserID, service, videoDuration, videoDurationParam, segments, userAgent } = preprocessInput(req);
|
||||
|
||||
const invalidCheckResult = checkInvalidFields(videoID, userID, segments);
|
||||
const invalidCheckResult = checkInvalidFields(videoID, paramUserID, segments);
|
||||
if (!invalidCheckResult.pass) {
|
||||
return res.status(invalidCheckResult.errorCode).send(invalidCheckResult.errorMessage);
|
||||
}
|
||||
|
||||
//hash the userID
|
||||
userID = await getHashCache(userID);
|
||||
const userID = await getHashCache(paramUserID);
|
||||
|
||||
const userWarningCheckResult = await checkUserActiveWarning(userID);
|
||||
if (!userWarningCheckResult.pass) {
|
||||
@@ -589,15 +601,15 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
return res.status(userWarningCheckResult.errorCode).send(userWarningCheckResult.errorMessage);
|
||||
}
|
||||
|
||||
//check if this user is on the vip list
|
||||
const isVIP = await isUserVIP(userID);
|
||||
const rawIP = getIP(req);
|
||||
|
||||
const newData = await updateDataIfVideoDurationChange(videoID, service, videoDuration, videoDurationParam);
|
||||
videoDuration = newData.videoDuration;
|
||||
const { lockedCategoryList, apiVideoInfo } = newData;
|
||||
|
||||
// Check if all submissions are correct
|
||||
const segmentCheckResult = await checkEachSegmentValid(userID, videoID, segments, service, isVIP, lockedCategoryList);
|
||||
const segmentCheckResult = await checkEachSegmentValid(rawIP, paramUserID, userID, videoID, segments, service, isVIP, lockedCategoryList);
|
||||
if (!segmentCheckResult.pass) {
|
||||
return res.status(segmentCheckResult.errorCode).send(segmentCheckResult.errorMessage);
|
||||
}
|
||||
@@ -616,7 +628,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
const newSegments = [];
|
||||
|
||||
//hash the ip 5000 times so no one can get it from the database
|
||||
const hashedIP = await getHashCache(getIP(req) + config.globalSalt);
|
||||
const hashedIP = await getHashCache(rawIP + config.globalSalt);
|
||||
|
||||
try {
|
||||
//get current time
|
||||
@@ -633,10 +645,16 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
const reputation = await getReputation(userID);
|
||||
|
||||
for (const segmentInfo of segments) {
|
||||
// Full segments are always rejected since there can only be one, so shadow hide wouldn't work
|
||||
if (segmentInfo.ignoreSegment
|
||||
|| (shadowBanRow.userCount && segmentInfo.actionType === ActionType.Full)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//this can just be a hash of the data
|
||||
//it's better than generating an actual UUID like what was used before
|
||||
//also better for duplication checking
|
||||
const UUID = getSubmissionUUID(videoID, segmentInfo.actionType, userID, parseFloat(segmentInfo.segment[0]), parseFloat(segmentInfo.segment[1]), service);
|
||||
const UUID = getSubmissionUUID(videoID, segmentInfo.category, segmentInfo.actionType, userID, parseFloat(segmentInfo.segment[0]), parseFloat(segmentInfo.segment[1]), service);
|
||||
const hashedVideoID = getHash(videoID, 1);
|
||||
|
||||
const startingLocked = isVIP ? 1 : 0;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getIP } from "../utils/getIP";
|
||||
import { getHashCache } from "../utils/getHashCache";
|
||||
import { config } from "../config";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, Visibility, VideoDuration } from "../types/segments.model";
|
||||
import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, Visibility, VideoDuration, ActionType } from "../types/segments.model";
|
||||
import { getCategoryActionType } from "../utils/categoryInfo";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import axios from "axios";
|
||||
@@ -188,38 +188,38 @@ async function sendWebhooks(voteData: VoteData) {
|
||||
}
|
||||
|
||||
async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, isOwnSubmission: boolean, category: Category
|
||||
, hashedIP: HashedIP, finalResponse: FinalResponse, res: Response): Promise<Response> {
|
||||
, hashedIP: HashedIP, finalResponse: FinalResponse): Promise<{ status: number, message?: string }> {
|
||||
// Check if they've already made a vote
|
||||
const usersLastVoteInfo = await privateDB.prepare("get", `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID]);
|
||||
|
||||
if (usersLastVoteInfo?.category === category) {
|
||||
// Double vote, ignore
|
||||
return res.sendStatus(finalResponse.finalStatus);
|
||||
return { status: finalResponse.finalStatus };
|
||||
}
|
||||
|
||||
const videoInfo = (await db.prepare("get", `SELECT "category", "videoID", "hashedVideoID", "service", "userID", "locked" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
||||
[UUID])) as {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
|
||||
if (!videoInfo) {
|
||||
// Submission doesn't exist
|
||||
return res.status(400).send("Submission doesn't exist.");
|
||||
return { status: 400, message: "Submission doesn't exist." };
|
||||
}
|
||||
|
||||
if (!config.categoryList.includes(category)) {
|
||||
return res.status(400).send("Category doesn't exist.");
|
||||
return { status: 400, message: "Category doesn't exist." };
|
||||
}
|
||||
if (getCategoryActionType(category) !== CategoryActionType.Skippable) {
|
||||
return res.status(400).send("Cannot vote for this category");
|
||||
return { status: 400, message: "Cannot vote for this category" };
|
||||
}
|
||||
|
||||
// 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" = ?`, [videoInfo.videoID, videoInfo.service, category]);
|
||||
if (nextCategoryLocked && !isVIP) {
|
||||
return res.sendStatus(200);
|
||||
return { status: 200 };
|
||||
}
|
||||
|
||||
// Ignore vote if the segment is locked
|
||||
if (!isVIP && videoInfo.locked === 1) {
|
||||
return res.sendStatus(200);
|
||||
return { status: 200 };
|
||||
}
|
||||
|
||||
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category]);
|
||||
@@ -279,7 +279,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
|
||||
QueryCacher.clearSegmentCache(videoInfo);
|
||||
|
||||
return res.sendStatus(finalResponse.finalStatus);
|
||||
return { status: finalResponse.finalStatus };
|
||||
}
|
||||
|
||||
export function getUserID(req: Request): UserID {
|
||||
@@ -289,16 +289,30 @@ export function getUserID(req: Request): UserID {
|
||||
export async function voteOnSponsorTime(req: Request, res: Response): Promise<Response> {
|
||||
const UUID = req.query.UUID as SegmentUUID;
|
||||
const paramUserID = getUserID(req);
|
||||
let type = req.query.type !== undefined ? parseInt(req.query.type as string) : undefined;
|
||||
const type = req.query.type !== undefined ? parseInt(req.query.type as string) : undefined;
|
||||
const category = req.query.category as Category;
|
||||
const ip = getIP(req);
|
||||
|
||||
const result = await vote(ip, UUID, paramUserID, type, category);
|
||||
|
||||
const response = res.status(result.status);
|
||||
if (result.message) {
|
||||
return response.send(result.message);
|
||||
} else if (result.json) {
|
||||
return response.json(result.json);
|
||||
} else {
|
||||
return response.send();
|
||||
}
|
||||
}
|
||||
|
||||
export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID, type: number, category?: Category): Promise<{ status: number, message?: string, json?: unknown }> {
|
||||
if (UUID === undefined || paramUserID === undefined || (type === undefined && category === undefined)) {
|
||||
//invalid request
|
||||
return res.sendStatus(400);
|
||||
return { status: 400 };
|
||||
}
|
||||
if (paramUserID.length < 30 && config.mode !== "test") {
|
||||
// Ignore this vote, invalid
|
||||
return res.sendStatus(200);
|
||||
return { status: 200 };
|
||||
}
|
||||
|
||||
//hash the userID
|
||||
@@ -314,9 +328,6 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
webhookMessage: null
|
||||
};
|
||||
|
||||
//x-forwarded-for if this server is behind a proxy
|
||||
const ip = getIP(req);
|
||||
|
||||
//hash the ip 5000 times so no one can get it from the database
|
||||
const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress);
|
||||
|
||||
@@ -331,7 +342,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
// disallow vote types 10/11
|
||||
if (type === 10 || type === 11) {
|
||||
// no longer allow type 10/11 alternative votes
|
||||
return res.sendStatus(400);
|
||||
return { status: 400 };
|
||||
}
|
||||
|
||||
// If not upvote
|
||||
@@ -350,19 +361,20 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
}
|
||||
|
||||
if (type === undefined && category !== undefined) {
|
||||
return categoryVote(UUID, nonAnonUserID, isVIP, isOwnSubmission, category, hashedIP, finalResponse, res);
|
||||
return categoryVote(UUID, nonAnonUserID, isVIP, isOwnSubmission, category, hashedIP, finalResponse);
|
||||
}
|
||||
|
||||
if (type !== undefined && !isVIP && !isOwnSubmission) {
|
||||
// Check if upvoting hidden segment
|
||||
const voteInfo = await db.prepare("get", `SELECT votes FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
||||
const voteInfo = await db.prepare("get", `SELECT votes, "actionType" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
|
||||
{ votes: number, actionType: ActionType };
|
||||
|
||||
if (voteInfo && voteInfo.votes <= -2) {
|
||||
if (voteInfo && voteInfo.votes <= -2 && voteInfo.actionType !== ActionType.Full) {
|
||||
if (type == 1) {
|
||||
return res.status(403).send("Not allowed to upvote segment with too many downvotes unless you are VIP.");
|
||||
return { status: 403, message: "Not allowed to upvote segment with too many downvotes unless you are VIP." };
|
||||
} else if (type == 0) {
|
||||
// Already downvoted enough, ignore
|
||||
return res.sendStatus(200);
|
||||
return { status: 200 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,9 +386,9 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
));
|
||||
|
||||
if (warnings.length >= config.maxNumberOfActiveWarnings) {
|
||||
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?" +
|
||||
`${(warnings[0]?.reason?.length > 0 ? ` Warning reason: '${warnings[0].reason}'` : "")}`);
|
||||
return { status: 403, message: "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?" +
|
||||
`${(warnings[0]?.reason?.length > 0 ? ` Warning reason: '${warnings[0].reason}'` : "")}` };
|
||||
}
|
||||
|
||||
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
|
||||
@@ -400,7 +412,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
incrementAmount = 0;
|
||||
} else {
|
||||
//unrecongnised type of vote
|
||||
return res.sendStatus(400);
|
||||
return { status: 400 };
|
||||
}
|
||||
if (votesRow != undefined) {
|
||||
if (votesRow.type === 1) {
|
||||
@@ -447,7 +459,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
|
||||
// Only change the database if they have made a submission before and haven't voted recently
|
||||
const ableToVote = isVIP
|
||||
|| (!(isOwnSubmission && incrementAmount > 0)
|
||||
|| (!(isOwnSubmission && incrementAmount > 0 && oldIncrementAmount >= 0)
|
||||
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined
|
||||
&& (await db.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)
|
||||
@@ -502,9 +514,9 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
finalResponse
|
||||
});
|
||||
}
|
||||
return res.status(finalResponse.finalStatus).send(finalResponse.finalMessage ?? undefined);
|
||||
return { status: finalResponse.finalStatus, message: finalResponse.finalMessage ?? undefined };
|
||||
} catch (err) {
|
||||
Logger.error(err as string);
|
||||
return res.status(500).json({ error: "Internal error creating segment vote" });
|
||||
return { status: 500, message: finalResponse.finalMessage ?? undefined, json: { error: "Internal error creating segment vote" } };
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ export type HashedIP = IPAddress & HashedValue;
|
||||
export enum ActionType {
|
||||
Skip = "skip",
|
||||
Mute = "mute",
|
||||
Chapter = "chapter"
|
||||
Chapter = "chapter",
|
||||
Full = "full"
|
||||
}
|
||||
|
||||
// Uncomment as needed
|
||||
@@ -32,6 +33,9 @@ export interface IncomingSegment {
|
||||
actionType: ActionType;
|
||||
segment: string[];
|
||||
description?: string;
|
||||
|
||||
// Used to remove in pre-check stage
|
||||
ignoreSegment?: boolean;
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
@@ -81,6 +85,7 @@ export interface OverlappingSegmentGroup {
|
||||
export interface VotableObject {
|
||||
votes: number;
|
||||
reputation: number;
|
||||
locked: boolean;
|
||||
}
|
||||
|
||||
export interface VotableObjectWithWeight extends VotableObject {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { getHash } from "./getHash";
|
||||
import { HashedValue } from "../types/hash.model";
|
||||
import { ActionType, VideoID, Service } from "../types/segments.model";
|
||||
import { ActionType, VideoID, Service, Category } from "../types/segments.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
|
||||
export function getSubmissionUUID(
|
||||
videoID: VideoID,
|
||||
category: Category,
|
||||
actionType: ActionType,
|
||||
userID: UserID,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
service: Service
|
||||
) : HashedValue {
|
||||
return `5${getHash(`${videoID}${startTime}${endTime}${userID}${actionType}${service}`, 1)}` as HashedValue;
|
||||
return `${getHash(`${videoID}${startTime}${endTime}${userID}${category}${actionType}${service}`, 1)}6` as HashedValue;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("getSkipSegmentsByHash", () => {
|
||||
const requiredSegmentHashVidHash = "17bf8d9090e050257772f8bff277293c29c7ce3b25eb969a8fae111a2434504d";
|
||||
const differentCategoryVidHash = "7fac44d1ee3257ec7f18953e2b5f991828de6854ad57193d1027c530981a89c0";
|
||||
const nonMusicOverlapVidHash = "306151f778f9bfd19872b3ccfc83cbab37c4f370717436bfd85e0a624cd8ba3c";
|
||||
const fullCategoryVidHash = "278fa987eebfe07ae3a4a60cf0663989ad874dd0c1f0430831d63c2001567e6f";
|
||||
before(async () => {
|
||||
const query = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", views, category, "actionType", "service", "hidden", "shadowHidden", "hashedVideoID", "description") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
await db.prepare("run", query, ["getSegmentsByHash-0", 1, 10, 2, 0, "getSegmentsByHash-01", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, getSegmentsByHash0Hash, ""]);
|
||||
@@ -50,6 +51,8 @@ describe("getSkipSegmentsByHash", () => {
|
||||
await db.prepare("run", query, ["differentCategoryVid", 60, 70, 2, 1, "differentCategoryVid-2", "testman", 0, 50, "intro", "skip", "YouTube", 0, 0, differentCategoryVidHash, ""]);
|
||||
await db.prepare("run", query, ["nonMusicOverlapVid", 60, 70, 2, 0, "nonMusicOverlapVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, nonMusicOverlapVidHash, ""]);
|
||||
await db.prepare("run", query, ["nonMusicOverlapVid", 60, 70, 2, 1, "nonMusicOverlapVid-2", "testman", 0, 50, "music_offtopic", "skip", "YouTube", 0, 0, nonMusicOverlapVidHash, ""]);
|
||||
await db.prepare("run", query, ["fullCategoryVid", 60, 70, 2, 0, "fullCategoryVid-1", "testman", 0, 50, "sponsor", "full", "YouTube", 0, 0, fullCategoryVidHash, ""]);
|
||||
await db.prepare("run", query, ["fullCategoryVid", 60, 70, 2, 1, "fullCategoryVid-2", "testman", 0, 50, "selfpromo", "full", "YouTube", 0, 0, fullCategoryVidHash, ""]);
|
||||
});
|
||||
|
||||
it("Should be able to get a 200", (done) => {
|
||||
@@ -528,6 +531,19 @@ describe("getSkipSegmentsByHash", () => {
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should only return one segment when fetching full video segments", (done) => {
|
||||
client.get(`${endpoint}/278f`, { params: { category: ["sponsor", "selfpromo"], actionType: "full" } })
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const data = res.data;
|
||||
assert.strictEqual(data.length, 1);
|
||||
assert.strictEqual(data[0].segments.length, 1);
|
||||
assert.strictEqual(data[0].segments[0].category, "selfpromo");
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to get specific segments with partial requiredSegments", (done) => {
|
||||
const requiredSegment1 = "fbf0af454059733c8822f6a4ac8ec568e0787f8c0a5ee915dd5b05e0d7a9a388";
|
||||
const requiredSegment2 = "7e1ebc5194551d2d0a606d64f675e5a14952e4576b2959f8c9d51e316c14f8da";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getSubmissionUUID } from "../../src/utils/getSubmissionUUID";
|
||||
import assert from "assert";
|
||||
import { ActionType, VideoID, Service } from "../../src/types/segments.model";
|
||||
import { ActionType, VideoID, Service, Category } from "../../src/types/segments.model";
|
||||
import { UserID } from "../../src/types/user.model";
|
||||
|
||||
describe("getSubmissionUUID", () => {
|
||||
it("Should return the hashed value", () => {
|
||||
assert.strictEqual(
|
||||
getSubmissionUUID("video001" as VideoID, "skip" as ActionType, "testuser001" as UserID, 13.33337, 42.000001, Service.YouTube),
|
||||
"529611b4cdd7319e705a32ae9557a02e59c8dbc1306097b2d2d5807c6405e9b1a");
|
||||
getSubmissionUUID("video001" as VideoID, "sponsor" as Category, "skip" as ActionType, "testuser001" as UserID, 13.33337, 42.000001, Service.YouTube),
|
||||
"2a473bca993dd84d8c2f6a4785989b20948dfe0c12c00f6f143bbda9ed561dca6");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,9 @@ import { db } from "../../src/databases/databases";
|
||||
import assert from "assert";
|
||||
import { LockCategory } from "../../src/types/segments.model";
|
||||
import { client } from "../utils/httpClient";
|
||||
import { partialDeepEquals } from "../utils/partialDeepEquals";
|
||||
|
||||
const stringDeepEquals = (a: string[] ,b: string[]): boolean => {
|
||||
const stringDeepEquals = (a: string[], b: string[]): boolean => {
|
||||
let result = true;
|
||||
b.forEach((e) => {
|
||||
if (!a.includes(e)) result = false;
|
||||
@@ -23,18 +24,27 @@ describe("lockCategoriesRecords", () => {
|
||||
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
|
||||
await db.prepare("run", insertVipUserQuery, [lockVIPUserHash]);
|
||||
|
||||
const insertLockCategoryQuery = 'INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason", "service") VALUES (?, ?, ?, ?, ?)';
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id", "sponsor", "reason-1", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id", "intro", "reason-1", "YouTube"]);
|
||||
const insertLockCategoryQuery = 'INSERT INTO "lockCategories" ("userID", "videoID", "actionType", "category", "reason", "service") VALUES (?, ?, ?, ?, ?, ?)';
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id", "skip", "sponsor", "reason-1", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id", "mute", "sponsor", "reason-1", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id", "skip", "intro", "reason-1", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id", "mute", "intro", "reason-1", "YouTube"]);
|
||||
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id-1", "sponsor", "reason-2", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id-1", "intro", "reason-2", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "lockCategoryVideo", "sponsor", "reason-3", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id-1", "skip", "sponsor", "reason-2", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id-1", "mute", "sponsor", "reason-2", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id-1", "skip", "intro", "reason-2", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "no-segments-video-id-1", "mute", "intro", "reason-2", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "lockCategoryVideo", "skip", "sponsor", "reason-3", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "lockCategoryVideo", "mute", "sponsor", "reason-3", "YouTube"]);
|
||||
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record", "sponsor", "reason-4", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "lockCategoryVideo-2", "skip", "sponsor", "reason-4", "YouTube"]);
|
||||
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record-1", "sponsor", "reason-5", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record-1", "intro", "reason-5", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record", "skip", "sponsor", "reason-4", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record", "mute", "sponsor", "reason-4", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record-1", "skip", "sponsor", "reason-5", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record-1", "mute", "sponsor", "reason-5", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record-1", "skip", "intro", "reason-5", "YouTube"]);
|
||||
await db.prepare("run", insertLockCategoryQuery, [lockVIPUserHash, "delete-record-1", "mute", "intro", "reason-5", "YouTube"]);
|
||||
});
|
||||
|
||||
it("Should update the database version when starting the application", async () => {
|
||||
@@ -60,6 +70,32 @@ describe("lockCategoriesRecords", () => {
|
||||
"outro",
|
||||
"shilling",
|
||||
],
|
||||
submittedValues: [
|
||||
{
|
||||
actionType: "skip",
|
||||
category: "outro"
|
||||
},
|
||||
{
|
||||
actionType: "mute",
|
||||
category: "outro"
|
||||
},
|
||||
{
|
||||
actionType: "skip",
|
||||
category: "shilling"
|
||||
},
|
||||
{
|
||||
actionType: "mute",
|
||||
category: "shilling"
|
||||
},
|
||||
{
|
||||
actionType: "skip",
|
||||
category: "intro"
|
||||
},
|
||||
{
|
||||
actionType: "mute",
|
||||
category: "intro"
|
||||
}
|
||||
]
|
||||
};
|
||||
client.post(endpoint, json)
|
||||
.then(res => {
|
||||
@@ -88,15 +124,15 @@ describe("lockCategoriesRecords", () => {
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const result = await checkLockCategories(videoID);
|
||||
assert.strictEqual(result.length, 4);
|
||||
assert.strictEqual(result.length, 8);
|
||||
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)
|
||||
);
|
||||
assert.strictEqual(newRecordWithEmptyReason.length, 2);
|
||||
assert.strictEqual(oldRecordNotChangeReason.length, 2);
|
||||
assert.strictEqual(newRecordWithEmptyReason.length, 4);
|
||||
assert.strictEqual(oldRecordNotChangeReason.length, 4);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
@@ -160,7 +196,7 @@ describe("lockCategoriesRecords", () => {
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const result = await checkLockCategories(videoID);
|
||||
assert.strictEqual(result.length, 4);
|
||||
assert.strictEqual(result.length, 8);
|
||||
const newRecordWithNewReason = result.filter(item =>
|
||||
expectedWithNewReason.includes(item.category) && item.reason === "new reason"
|
||||
);
|
||||
@@ -168,8 +204,8 @@ describe("lockCategoriesRecords", () => {
|
||||
item.reason === "reason-2"
|
||||
);
|
||||
|
||||
assert.strictEqual(newRecordWithNewReason.length, 3);
|
||||
assert.strictEqual(oldRecordNotChangeReason.length, 1);
|
||||
assert.strictEqual(newRecordWithNewReason.length, 6);
|
||||
assert.strictEqual(oldRecordNotChangeReason.length, 2);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
@@ -187,7 +223,7 @@ describe("lockCategoriesRecords", () => {
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const result = await checkLockCategories("underscore");
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(result.length, 2);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
@@ -205,7 +241,7 @@ describe("lockCategoriesRecords", () => {
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const result = await checkLockCategories("bothCases");
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(result.length, 2);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
@@ -231,6 +267,41 @@ describe("lockCategoriesRecords", () => {
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to submit specific action type not in video (sql check)", (done) => {
|
||||
const videoID = "lockCategoryVideo-2";
|
||||
const json = {
|
||||
videoID,
|
||||
userID: lockVIPUser,
|
||||
categories: [
|
||||
"sponsor",
|
||||
],
|
||||
actionTypes: [
|
||||
"mute"
|
||||
],
|
||||
reason: "custom-reason",
|
||||
};
|
||||
client.post(endpoint, json)
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const result = await checkLockCategories(videoID);
|
||||
assert.strictEqual(result.length, 2);
|
||||
assert.ok(partialDeepEquals(result, [
|
||||
{
|
||||
category: "sponsor",
|
||||
actionType: "skip",
|
||||
reason: "reason-4",
|
||||
},
|
||||
{
|
||||
category: "sponsor",
|
||||
actionType: "mute",
|
||||
reason: "custom-reason",
|
||||
}
|
||||
]));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return 400 for missing params", (done) => {
|
||||
client.post(endpoint, {})
|
||||
.then(res => {
|
||||
@@ -365,7 +436,7 @@ describe("lockCategoriesRecords", () => {
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const result = await checkLockCategories(videoID);
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(result.length, 2);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
|
||||
@@ -16,6 +16,7 @@ describe("postSkipSegments", () => {
|
||||
// Constant and helpers
|
||||
const submitUserOne = `PostSkipUser1${".".repeat(18)}`;
|
||||
const submitUserTwo = `PostSkipUser2${".".repeat(18)}`;
|
||||
const submitUserTwoHash = getHash(submitUserTwo);
|
||||
const submitUserThree = `PostSkipUser3${".".repeat(18)}`;
|
||||
|
||||
const warnUser01 = "warn-user01-qwertyuiopasdfghjklzxcvbnm";
|
||||
@@ -34,8 +35,9 @@ describe("postSkipSegments", () => {
|
||||
const warnVideoID = "postSkip2";
|
||||
const badInputVideoID = "dQw4w9WgXcQ";
|
||||
const shadowBanVideoID = "postSkipBan";
|
||||
const shadowBanVideoID2 = "postSkipBan2";
|
||||
|
||||
const queryDatabase = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
const queryDatabase = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "votes", "userID", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
const queryDatabaseActionType = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
const queryDatabaseChapter = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "actionType", "description" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
const queryDatabaseDuration = (videoID: string) => db.prepare("get", `SELECT "startTime", "endTime", "locked", "category", "videoDuration" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
@@ -54,10 +56,15 @@ describe("postSkipSegments", () => {
|
||||
});
|
||||
|
||||
before(() => {
|
||||
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 1000, 0, "80percent-uuid-0", submitUserOneHash, 0, 0, "interaction", 0, "80percent_video"]);
|
||||
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 1001, 1005, 0, "80percent-uuid-1", submitUserOneHash, 0, 0, "interaction", 0, "80percent_video"]);
|
||||
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 5000, -2, "80percent-uuid-2", submitUserOneHash, 0, 0, "interaction", 0, "80percent_video"]);
|
||||
const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "UUID", "userID", "timeSubmitted", views, category, "actionType", "videoDuration", "shadowHidden", "hashedVideoID") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 1000, 0, "80percent-uuid-0", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]);
|
||||
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 1001, 1005, 0, "80percent-uuid-1", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]);
|
||||
db.prepare("run", insertSponsorTimeQuery, ["80percent_video", 0, 5000, -2, "80percent-uuid-2", submitUserOneHash, 0, 0, "interaction", "skip", 0, 0, "80percent_video"]);
|
||||
|
||||
db.prepare("run", insertSponsorTimeQuery, ["full_video_segment", 0, 0, 0, "full-video-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 0, 0, "full_video_segment"]);
|
||||
|
||||
db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 0, 0, 0, "full-video-duration-uuid-0", submitUserTwoHash, 0, 0, "sponsor", "full", 123, 0, "full_video_duration_segment"]);
|
||||
db.prepare("run", insertSponsorTimeQuery, ["full_video_duration_segment", 25, 30, 0, "full-video-duration-uuid-1", submitUserTwoHash, 0, 0, "sponsor", "skip", 123, 0, "full_video_duration_segment"]);
|
||||
|
||||
const now = Date.now();
|
||||
const warnVip01Hash = getHash("warn-vip01-qwertyuiopasdfghjklzxcvbnm");
|
||||
@@ -405,6 +412,39 @@ describe("postSkipSegments", () => {
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should be able to submit with a new duration, and not hide full video segments", async () => {
|
||||
const videoID = "full_video_duration_segment";
|
||||
const res = await postSkipSegmentJSON({
|
||||
userID: submitUserOne,
|
||||
videoID,
|
||||
videoDuration: 100,
|
||||
segments: [{
|
||||
segment: [20, 30],
|
||||
category: "sponsor",
|
||||
}],
|
||||
});
|
||||
assert.strictEqual(res.status, 200);
|
||||
const videoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "actionType", "videoDuration"
|
||||
FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 0`, [videoID]);
|
||||
const hiddenVideoRows = await db.prepare("all", `SELECT "startTime", "endTime", "locked", "category", "videoDuration"
|
||||
FROM "sponsorTimes" WHERE "videoID" = ? AND hidden = 1`, [videoID]);
|
||||
assert.strictEqual(videoRows.length, 2);
|
||||
const expected = {
|
||||
startTime: 20,
|
||||
endTime: 30,
|
||||
locked: 0,
|
||||
category: "sponsor",
|
||||
videoDuration: 100
|
||||
};
|
||||
assert.ok(partialDeepEquals(videoRows[1], expected));
|
||||
const fullExpected = {
|
||||
category: "sponsor",
|
||||
actionType: "full"
|
||||
};
|
||||
assert.ok(partialDeepEquals(videoRows[0], fullExpected));
|
||||
assert.strictEqual(hiddenVideoRows.length, 1);
|
||||
});
|
||||
|
||||
it("Should be able to submit a single time under a different service (JSON method)", (done) => {
|
||||
const videoID = "postSkip7";
|
||||
postSkipSegmentJSON({
|
||||
@@ -901,6 +941,26 @@ describe("postSkipSegments", () => {
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return not be 403 when submitting with locked category but unlocked actionType", (done) => {
|
||||
const videoID = "lockedVideo";
|
||||
db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason")
|
||||
VALUES(?, ?, ?, ?)`, [getHash("VIPUser-lockCategories"), videoID, "sponsor", "Custom Reason"])
|
||||
.then(() => postSkipSegmentJSON({
|
||||
userID: submitUserOne,
|
||||
videoID,
|
||||
segments: [{
|
||||
segment: [1, 10],
|
||||
category: "sponsor",
|
||||
actionType: "mute"
|
||||
}],
|
||||
}))
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return 403 for submiting in lockedCategory", (done) => {
|
||||
const videoID = "lockedVideo1";
|
||||
db.prepare("run", `INSERT INTO "lockCategories" ("userID", "videoID", "category", "reason")
|
||||
@@ -1044,6 +1104,77 @@ describe("postSkipSegments", () => {
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should allow submitting full video sponsor", (done) => {
|
||||
const videoID = "qqwerth";
|
||||
postSkipSegmentParam({
|
||||
videoID,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
category: "sponsor",
|
||||
actionType: "full",
|
||||
userID: submitUserTwo
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const row = await queryDatabase(videoID);
|
||||
const expected = {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
votes: 0,
|
||||
userID: submitUserTwoHash,
|
||||
category: "sponsor",
|
||||
actionType: "full"
|
||||
};
|
||||
assert.ok(partialDeepEquals(row, expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Submitting duplicate full video sponsor should count as an upvote", (done) => {
|
||||
const videoID = "full_video_segment";
|
||||
postSkipSegmentParam({
|
||||
videoID,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
category: "sponsor",
|
||||
actionType: "full",
|
||||
userID: submitUserOne
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const row = await queryDatabase(videoID);
|
||||
const expected = {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
votes: 1,
|
||||
userID: submitUserTwoHash,
|
||||
category: "sponsor",
|
||||
actionType: "full"
|
||||
};
|
||||
assert.ok(partialDeepEquals(row, expected));
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should not allow submitting full video sponsor not at zero seconds", (done) => {
|
||||
const videoID = "qqwerth";
|
||||
postSkipSegmentParam({
|
||||
videoID,
|
||||
startTime: 0,
|
||||
endTime: 1,
|
||||
category: "sponsor",
|
||||
actionType: "full",
|
||||
userID: submitUserTwo
|
||||
})
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 400);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should not be able to submit with colons in timestamps", (done) => {
|
||||
const videoID = "colon-1";
|
||||
postSkipSegmentJSON({
|
||||
@@ -1085,6 +1216,25 @@ describe("postSkipSegments", () => {
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should not add full segments to database if user if shadowbanned", (done) => {
|
||||
const videoID = shadowBanVideoID2;
|
||||
postSkipSegmentParam({
|
||||
videoID,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
category: "sponsor",
|
||||
actionType: "full",
|
||||
userID: banUser01
|
||||
})
|
||||
.then(async res => {
|
||||
assert.strictEqual(res.status, 200);
|
||||
const row = await db.prepare("get", `SELECT "startTime", "endTime", "shadowHidden", "userID" FROM "sponsorTimes" WHERE "videoID" = ?`, [videoID]);
|
||||
assert.strictEqual(row, undefined);
|
||||
done();
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
|
||||
it("Should return 400 if videoID is empty", (done) => {
|
||||
const videoID = null as string;
|
||||
postSkipSegmentParam({
|
||||
|
||||
@@ -13,7 +13,7 @@ export class YouTubeApiMock {
|
||||
};
|
||||
}
|
||||
|
||||
if (obj.id === "noDuration") {
|
||||
if (obj.id === "noDuration" || obj.id === "full_video_duration_segment") {
|
||||
return {
|
||||
err: null,
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user