Merge pull request #440 from ajayyy/full-video-labels

Full video labeling
This commit is contained in:
Ajay Ramachandran
2022-01-06 15:55:11 -05:00
committed by GitHub
14 changed files with 438 additions and 125 deletions

View File

@@ -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"],

View File

@@ -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);

View File

@@ -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;
});
}

View File

@@ -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;

View File

@@ -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" } };
}
}

View File

@@ -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 {

View File

@@ -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;
}