mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-15 07:57:05 +03:00
Merge pull request #440 from ajayyy/full-video-labels
Full video labeling
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user