mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-26 09:28:34 +03:00
Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer
This commit is contained in:
@@ -17,6 +17,19 @@ import { QueryCacher } from "../utils/queryCacher";
|
|||||||
import { getReputation } from "../utils/reputation";
|
import { getReputation } from "../utils/reputation";
|
||||||
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
|
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
|
||||||
import { UserID } from "../types/user.model";
|
import { UserID } from "../types/user.model";
|
||||||
|
import { isUserVIP } from "../utils/isUserVIP";
|
||||||
|
|
||||||
|
type CheckResult = {
|
||||||
|
pass: boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
errorCode: number
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHECK_PASS: CheckResult = {
|
||||||
|
pass: true,
|
||||||
|
errorMessage: "",
|
||||||
|
errorCode: 0
|
||||||
|
};
|
||||||
|
|
||||||
async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: APIVideoData, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
|
async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: APIVideoData, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
|
||||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||||
@@ -267,7 +280,7 @@ async function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkUserActiveWarning(userID: string): Promise<{ pass: boolean; errorMessage: string; }> {
|
async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
|
||||||
const MILLISECONDS_IN_HOUR = 3600000;
|
const MILLISECONDS_IN_HOUR = 3600000;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const warnings = await db.prepare("all",
|
const warnings = await db.prepare("all",
|
||||||
@@ -291,56 +304,15 @@ async function checkUserActiveWarning(userID: string): Promise<{ pass: boolean;
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
errorMessage: defaultMessage + (warnings[0]?.reason?.length > 0 ? `\n\nWarning reason: ${warnings[0].reason}` : "")
|
errorMessage: defaultMessage + (warnings[0]?.reason?.length > 0 ? `\n\nWarning reason: ${warnings[0].reason}` : ""),
|
||||||
|
errorCode: 403
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {pass: true, errorMessage: ""};
|
return CHECK_PASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function proxySubmission(req: Request) {
|
function checkInvalidFields(videoID: any, userID: any, segments: Array<any>): CheckResult {
|
||||||
fetch(`${config.proxySubmission}/api/skipSegments?userID=${req.query.userID}&videoID=${req.query.videoID}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: req.body,
|
|
||||||
})
|
|
||||||
.then(async res => {
|
|
||||||
Logger.debug(`Proxy Submission: ${res.status} (${(await res.text())})`);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
Logger.error("Proxy Submission: Failed to make call");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function postSkipSegments(req: Request, res: Response): Promise<Response> {
|
|
||||||
if (config.proxySubmission) {
|
|
||||||
proxySubmission(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoID = req.query.videoID || req.body.videoID;
|
|
||||||
let userID = req.query.userID || req.body.userID;
|
|
||||||
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube;
|
|
||||||
if (!Object.values(Service).some((val) => val === service)) {
|
|
||||||
service = Service.YouTube;
|
|
||||||
}
|
|
||||||
const videoDurationParam: VideoDuration = (parseFloat(req.query.videoDuration || req.body.videoDuration) || 0) as VideoDuration;
|
|
||||||
let videoDuration = videoDurationParam;
|
|
||||||
|
|
||||||
let segments = req.body.segments as IncomingSegment[];
|
|
||||||
if (segments === undefined) {
|
|
||||||
// Use query instead
|
|
||||||
segments = [{
|
|
||||||
segment: [req.query.startTime as string, req.query.endTime as string],
|
|
||||||
category: req.query.category as Category,
|
|
||||||
actionType: (req.query.actionType as ActionType) ?? ActionType.Skip
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
// Add default action type
|
|
||||||
segments.forEach((segment) => {
|
|
||||||
if (!Object.values(ActionType).some((val) => val === segment.actionType)){
|
|
||||||
segment.actionType = ActionType.Skip;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const invalidFields = [];
|
const invalidFields = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
if (typeof videoID !== "string") {
|
if (typeof videoID !== "string") {
|
||||||
@@ -358,25 +330,99 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||||||
// invalid request
|
// invalid request
|
||||||
const formattedFields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ", " : "") + c, "");
|
const formattedFields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ", " : "") + c, "");
|
||||||
const formattedErrors = errors.reduce((p, c, i) => p + (i !== 0 ? ". " : " ") + c, "");
|
const formattedErrors = errors.reduce((p, c, i) => p + (i !== 0 ? ". " : " ") + c, "");
|
||||||
return res.status(400).send(`No valid ${formattedFields} field(s) provided.${formattedErrors}`);
|
return {
|
||||||
|
pass: false,
|
||||||
|
errorMessage: `No valid ${formattedFields} field(s) provided.${formattedErrors}`,
|
||||||
|
errorCode: 400
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//hash the userID
|
return CHECK_PASS;
|
||||||
userID = getHash(userID);
|
}
|
||||||
|
|
||||||
const warningResult: {pass: boolean, errorMessage: string} = await checkUserActiveWarning(userID);
|
async function checkEachSegmentValid(userID: string, videoID: VideoID
|
||||||
if (!warningResult.pass) {
|
, segments: Array<any>, service: string, isVIP: boolean, lockedCategoryList: Array<any>): Promise<CheckResult> {
|
||||||
Logger.warn(`Caught a submission for for a warned user. userID: '${userID}', videoID: '${videoID}', category: '${segments.reduce<string>((prev, val) => `${prev} ${val.category}`, "")}', times: ${segments.reduce<string>((prev, val) => `${prev} ${val.segment}`, "")}`);
|
|
||||||
return res.status(403).send(warningResult.errorMessage);
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
if (segments[i] === undefined || segments[i].segment === undefined || segments[i].category === undefined) {
|
||||||
|
//invalid request
|
||||||
|
return { pass: false, errorMessage: "One of your segments are invalid", errorCode: 400};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.categoryList.includes(segments[i].category)) {
|
||||||
|
return { pass: false, errorMessage: "Category doesn't exist.", errorCode: 400};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject segment if it's in the locked categories list
|
||||||
|
if (!isVIP && lockedCategoryList.indexOf(segments[i].category) !== -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}`);
|
||||||
|
return { pass: false, errorCode: 403,
|
||||||
|
errorMessage: `New submissions are not allowed for the following category: \
|
||||||
|
'${segments[i].category}'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n\
|
||||||
|
${(segments[i].category === "sponsor" ? "Maybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. "+
|
||||||
|
"Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n\n" : "")}\
|
||||||
|
If you believe this is incorrect, please contact someone on discord.gg/SponsorBlock or matrix.to/#/+sponsor:ajay.app`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = parseFloat(segments[i].segment[0]);
|
||||||
|
const endTime = parseFloat(segments[i].segment[1]);
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
//invalid request
|
||||||
|
return { pass: false, errorMessage: "One of your segments times are invalid (too short, startTime before endTime, etc.)", errorCode: 400};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVIP && segments[i].category === "sponsor" && Math.abs(startTime - endTime) < 1) {
|
||||||
|
// Too short
|
||||||
|
return { pass: false, errorMessage: "Sponsors 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" = ?
|
||||||
|
and "endTime" = ? and "category" = ? and "videoID" = ? and "service" = ?`, [startTime, endTime, segments[i].category, videoID, service]);
|
||||||
|
if (duplicateCheck2Row.count > 0) {
|
||||||
|
return { pass: false, errorMessage: "Sponsors has already been submitted before.", errorCode: 409};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CHECK_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkByAutoModerator(videoID: any, userID: any, segments: Array<any>, isVIP: boolean, service:string, apiVideoInfo: APIVideoInfo, decreaseVotes: number): Promise<CheckResult & { decreaseVotes: number; } > {
|
||||||
|
// Auto moderator check
|
||||||
|
if (!isVIP && service == Service.YouTube) {
|
||||||
|
const autoModerateResult = await autoModerateSubmission(apiVideoInfo, {userID, videoID, segments});//startTime, endTime, category: segments[i].category});
|
||||||
|
if (autoModerateResult == "Rejected based on NeuralBlock predictions.") {
|
||||||
|
// If NB automod rejects, the submission will start with -2 votes.
|
||||||
|
// Note, if one submission is bad all submissions will be affected.
|
||||||
|
// However, this behavior is consistent with other automod functions
|
||||||
|
// already in place.
|
||||||
|
//decreaseVotes = -2; //Disable for now
|
||||||
|
} else if (autoModerateResult) {
|
||||||
|
//Normal automod behavior
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
errorCode: 403,
|
||||||
|
errorMessage: `Request rejected by auto moderator: ${autoModerateResult} If this is an issue, send a message on Discord.`,
|
||||||
|
decreaseVotes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...CHECK_PASS,
|
||||||
|
decreaseVotes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDataIfVideoDurationChange(videoID: VideoID, service: string, videoDuration: VideoDuration, videoDurationParam: VideoDuration) {
|
||||||
let lockedCategoryList = (await db.prepare("all", 'SELECT category from "lockCategories" where "videoID" = ?', [videoID])).map((list: any) => list.category );
|
let lockedCategoryList = (await db.prepare("all", 'SELECT category from "lockCategories" where "videoID" = ?', [videoID])).map((list: any) => list.category );
|
||||||
|
|
||||||
//check if this user is on the vip list
|
|
||||||
const isVIP = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [userID])).userCount > 0;
|
|
||||||
|
|
||||||
const decreaseVotes = 0;
|
|
||||||
|
|
||||||
const previousSubmissions = await db.prepare("all",
|
const previousSubmissions = await db.prepare("all",
|
||||||
`SELECT "videoDuration", "UUID"
|
`SELECT "videoDuration", "UUID"
|
||||||
FROM "sponsorTimes"
|
FROM "sponsorTimes"
|
||||||
@@ -388,7 +434,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||||||
|
|
||||||
// If the video's duration is changed, then the video should be unlocked and old submissions should be hidden
|
// If the video's duration is changed, then the video should be unlocked and old submissions should be hidden
|
||||||
const videoDurationChanged = (videoDuration: number) => videoDuration != 0
|
const videoDurationChanged = (videoDuration: number) => videoDuration != 0
|
||||||
&& previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
|
&& previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
|
||||||
|
|
||||||
let apiVideoInfo: APIVideoInfo = null;
|
let apiVideoInfo: APIVideoInfo = null;
|
||||||
if (service == Service.YouTube) {
|
if (service == Service.YouTube) {
|
||||||
@@ -407,75 +453,145 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||||||
for (const submission of previousSubmissions) {
|
for (const submission of previousSubmissions) {
|
||||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]);
|
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset lock categories
|
|
||||||
lockedCategoryList = [];
|
lockedCategoryList = [];
|
||||||
deleteLockCategories(videoID, null);
|
deleteLockCategories(videoID, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
videoDuration,
|
||||||
|
apiVideoInfo,
|
||||||
|
lockedCategoryList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable max submissions for now
|
||||||
|
// Disable IP ratelimiting for now
|
||||||
|
async function checkRateLimit(userID:string, videoID: VideoID, timeSubmitted: number, hashedIP: string, options: {
|
||||||
|
enableCheckByIP: boolean;
|
||||||
|
enableCheckByUserID: boolean;
|
||||||
|
} = {
|
||||||
|
enableCheckByIP: false,
|
||||||
|
enableCheckByUserID: false
|
||||||
|
}): Promise<CheckResult> {
|
||||||
|
const yesterday = timeSubmitted - 86400000;
|
||||||
|
|
||||||
|
if (options.enableCheckByIP) {
|
||||||
|
//check to see if this ip has submitted too many sponsors today
|
||||||
|
const rateLimitCheckRow = await privateDB.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "hashedIP" = ? AND "videoID" = ? AND "timeSubmitted" > ?`, [hashedIP, videoID, yesterday]);
|
||||||
|
|
||||||
|
if (rateLimitCheckRow.count >= 10) {
|
||||||
|
//too many sponsors for the same video from the same ip address
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
errorCode: 429,
|
||||||
|
errorMessage: "Have submited many sponsors for the same video."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enableCheckByUserID) {
|
||||||
|
//check to see if the user has already submitted sponsors for this video
|
||||||
|
const duplicateCheckRow = await db.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "userID" = ? and "videoID" = ?`, [userID, videoID]);
|
||||||
|
|
||||||
|
if (duplicateCheckRow.count >= 16) {
|
||||||
|
//too many sponsors for the same video from the same user
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
errorCode: 429,
|
||||||
|
errorMessage: "Have submited many sponsors for the same video."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CHECK_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxySubmission(req: Request) {
|
||||||
|
fetch(`${config.proxySubmission}/api/skipSegments?userID=${req.query.userID}&videoID=${req.query.videoID}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: req.body,
|
||||||
|
})
|
||||||
|
.then(async res => {
|
||||||
|
Logger.debug(`Proxy Submission: ${res.status} (${(await res.text())})`);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
Logger.error("Proxy Submission: Failed to make call");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preprocessInput(req: Request) {
|
||||||
|
const videoID = req.query.videoID || req.body.videoID;
|
||||||
|
const userID = req.query.userID || req.body.userID;
|
||||||
|
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube;
|
||||||
|
if (!Object.values(Service).some((val) => val === service)) {
|
||||||
|
service = Service.YouTube;
|
||||||
|
}
|
||||||
|
const videoDurationParam: VideoDuration = (parseFloat(req.query.videoDuration || req.body.videoDuration) || 0) as VideoDuration;
|
||||||
|
const videoDuration = videoDurationParam;
|
||||||
|
|
||||||
|
let segments = req.body.segments as IncomingSegment[];
|
||||||
|
if (segments === undefined) {
|
||||||
|
// Use query instead
|
||||||
|
segments = [{
|
||||||
|
segment: [req.query.startTime as string, req.query.endTime as string],
|
||||||
|
category: req.query.category as Category,
|
||||||
|
actionType: (req.query.actionType as ActionType) ?? ActionType.Skip
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
// Add default action type
|
||||||
|
segments.forEach((segment) => {
|
||||||
|
if (!Object.values(ActionType).some((val) => val === segment.actionType)){
|
||||||
|
segment.actionType = ActionType.Skip;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {videoID, userID, service, videoDuration, videoDurationParam, segments};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postSkipSegments(req: Request, res: Response): Promise<Response> {
|
||||||
|
if (config.proxySubmission) {
|
||||||
|
proxySubmission(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let {videoID, userID, service, videoDuration, videoDurationParam, segments} = preprocessInput(req);
|
||||||
|
|
||||||
|
const invalidCheckResult = checkInvalidFields(videoID, userID, segments);
|
||||||
|
if (!invalidCheckResult.pass) {
|
||||||
|
return res.status(invalidCheckResult.errorCode).send(invalidCheckResult.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
//hash the userID
|
||||||
|
userID = getHash(userID);
|
||||||
|
|
||||||
|
const userWarningCheckResult = await checkUserActiveWarning(userID);
|
||||||
|
if (!userWarningCheckResult.pass) {
|
||||||
|
Logger.warn(`Caught a submission for for a warned user. userID: '${userID}', videoID: '${videoID}', category: '${segments.reduce<string>((prev, val) => `${prev} ${val.category}`, "")}', times: ${segments.reduce<string>((prev, val) => `${prev} ${val.segment}`, "")}`);
|
||||||
|
return res.status(userWarningCheckResult.errorCode).send(userWarningCheckResult.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
//check if this user is on the vip list
|
||||||
|
const isVIP = await isUserVIP(userID);
|
||||||
|
|
||||||
|
const newData = await updateDataIfVideoDurationChange(videoID, service, videoDuration, videoDurationParam);
|
||||||
|
videoDuration = newData.videoDuration;
|
||||||
|
const { lockedCategoryList, apiVideoInfo } = newData;
|
||||||
|
|
||||||
// Check if all submissions are correct
|
// Check if all submissions are correct
|
||||||
for (let i = 0; i < segments.length; i++) {
|
const segmentCheckResult = await checkEachSegmentValid(userID, videoID, segments, service, isVIP, lockedCategoryList);
|
||||||
if (segments[i] === undefined || segments[i].segment === undefined || segments[i].category === undefined) {
|
if (!segmentCheckResult.pass) {
|
||||||
//invalid request
|
return res.status(segmentCheckResult.errorCode).send(segmentCheckResult.errorMessage);
|
||||||
return res.status(400).send("One of your segments are invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.categoryList.includes(segments[i].category)) {
|
|
||||||
return res.status(400).send("Category doesn't exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject segment if it's in the locked categories list
|
|
||||||
if (!isVIP && lockedCategoryList.indexOf(segments[i].category) !== -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}`);
|
|
||||||
return res.status(403).send(
|
|
||||||
`New submissions are not allowed for the following category: \
|
|
||||||
'${segments[i].category}'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n\
|
|
||||||
${(segments[i].category === "sponsor" ? "Maybe the segment you are submitting is a different category that you have not enabled and is not a sponsor. "+
|
|
||||||
"Categories that aren't sponsor, such as self-promotion can be enabled in the options.\n\n" : "")}\
|
|
||||||
If you believe this is incorrect, please contact someone on discord.gg/SponsorBlock or matrix.to/#/+sponsor:ajay.app`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const startTime = parseFloat(segments[i].segment[0]);
|
|
||||||
const endTime = parseFloat(segments[i].segment[1]);
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
//invalid request
|
|
||||||
return res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isVIP && segments[i].category === "sponsor" && Math.abs(startTime - endTime) < 1) {
|
|
||||||
// Too short
|
|
||||||
return res.status(400).send("Sponsors must be longer than 1 second long");
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if this info has already been submitted before
|
|
||||||
const duplicateCheck2Row = await db.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "startTime" = ?
|
|
||||||
and "endTime" = ? and "category" = ? and "videoID" = ? and "service" = ?`, [startTime, endTime, segments[i].category, videoID, service]);
|
|
||||||
if (duplicateCheck2Row.count > 0) {
|
|
||||||
return res.sendStatus(409);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto moderator check
|
let decreaseVotes = 0;
|
||||||
if (!isVIP && service == Service.YouTube) {
|
// Auto check by NB
|
||||||
const autoModerateResult = await autoModerateSubmission(apiVideoInfo, {userID, videoID, segments});//startTime, endTime, category: segments[i].category});
|
const autoModerateCheckResult = await checkByAutoModerator(videoID, userID, segments, isVIP, service, apiVideoInfo, decreaseVotes);
|
||||||
if (autoModerateResult == "Rejected based on NeuralBlock predictions.") {
|
if (!autoModerateCheckResult.pass) {
|
||||||
// If NB automod rejects, the submission will start with -2 votes.
|
return res.status(autoModerateCheckResult.errorCode).send(autoModerateCheckResult.errorMessage);
|
||||||
// Note, if one submission is bad all submissions will be affected.
|
} else {
|
||||||
// However, this behavior is consistent with other automod functions
|
decreaseVotes = autoModerateCheckResult.decreaseVotes;
|
||||||
// already in place.
|
|
||||||
//decreaseVotes = -2; //Disable for now
|
|
||||||
} else if (autoModerateResult) {
|
|
||||||
//Normal automod behavior
|
|
||||||
return res.status(403).send(`Request rejected by auto moderator: ${autoModerateResult} If this is an issue, send a message on Discord.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will be filled when submitting
|
// Will be filled when submitting
|
||||||
const UUIDs = [];
|
const UUIDs = [];
|
||||||
const newSegments = [];
|
const newSegments = [];
|
||||||
@@ -487,31 +603,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
|||||||
//get current time
|
//get current time
|
||||||
const timeSubmitted = Date.now();
|
const timeSubmitted = Date.now();
|
||||||
|
|
||||||
const yesterday = timeSubmitted - 86400000;
|
// const rateLimitCheckResult = checkRateLimit(userID, videoID, timeSubmitted, hashedIP);
|
||||||
|
// if (!rateLimitCheckResult.pass) {
|
||||||
// Disable IP ratelimiting for now
|
// return res.status(rateLimitCheckResult.errorCode).send(rateLimitCheckResult.errorMessage);
|
||||||
// eslint-disable-next-line no-constant-condition
|
// }
|
||||||
if (false) {
|
|
||||||
//check to see if this ip has submitted too many sponsors today
|
|
||||||
const rateLimitCheckRow = await privateDB.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "hashedIP" = ? AND "videoID" = ? AND "timeSubmitted" > ?`, [hashedIP, videoID, yesterday]);
|
|
||||||
|
|
||||||
if (rateLimitCheckRow.count >= 10) {
|
|
||||||
//too many sponsors for the same video from the same ip address
|
|
||||||
return res.sendStatus(429);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable max submissions for now
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
if (false) {
|
|
||||||
//check to see if the user has already submitted sponsors for this video
|
|
||||||
const duplicateCheckRow = await db.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "userID" = ? and "videoID" = ?`, [userID, videoID]);
|
|
||||||
|
|
||||||
if (duplicateCheckRow.count >= 16) {
|
|
||||||
//too many sponsors for the same video from the same user
|
|
||||||
return res.sendStatus(429);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//check to see if this user is shadowbanned
|
//check to see if this user is shadowbanned
|
||||||
const shadowBanRow = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
const shadowBanRow = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import {db} from "../databases/databases";
|
|||||||
import { HashedUserID } from "../types/user.model";
|
import { HashedUserID } from "../types/user.model";
|
||||||
|
|
||||||
export async function isUserVIP(userID: HashedUserID): Promise<boolean> {
|
export async function isUserVIP(userID: HashedUserID): Promise<boolean> {
|
||||||
return (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [userID])).userCount > 0;
|
return (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ? LIMIT 1`, [userID])).userCount > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ interface ReputationDBResult {
|
|||||||
votedSum: number,
|
votedSum: number,
|
||||||
lockedSum: number,
|
lockedSum: number,
|
||||||
semiOldUpvotedSubmissions: number,
|
semiOldUpvotedSubmissions: number,
|
||||||
oldUpvotedSubmissions: number
|
oldUpvotedSubmissions: number,
|
||||||
|
mostUpvotedInLockedVideoSum: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReputation(userID: UserID): Promise<number> {
|
export async function getReputation(userID: UserID): Promise<number> {
|
||||||
@@ -28,43 +29,60 @@ export async function getReputation(userID: UserID): Promise<number> {
|
|||||||
SUM(CASE WHEN "timeSubmitted" > 1596240000000 THEN "votes" ELSE 0 END) AS "votedSum",
|
SUM(CASE WHEN "timeSubmitted" > 1596240000000 THEN "votes" ELSE 0 END) AS "votedSum",
|
||||||
SUM(locked) AS "lockedSum",
|
SUM(locked) AS "lockedSum",
|
||||||
SUM(CASE WHEN "timeSubmitted" < ? AND "timeSubmitted" > 1596240000000 AND "votes" > 0 THEN 1 ELSE 0 END) AS "semiOldUpvotedSubmissions",
|
SUM(CASE WHEN "timeSubmitted" < ? AND "timeSubmitted" > 1596240000000 AND "votes" > 0 THEN 1 ELSE 0 END) AS "semiOldUpvotedSubmissions",
|
||||||
SUM(CASE WHEN "timeSubmitted" < ? AND "timeSubmitted" > 1596240000000 AND "votes" > 0 THEN 1 ELSE 0 END) AS "oldUpvotedSubmissions"
|
SUM(CASE WHEN "timeSubmitted" < ? AND "timeSubmitted" > 1596240000000 AND "votes" > 0 THEN 1 ELSE 0 END) AS "oldUpvotedSubmissions",
|
||||||
|
SUM(CASE WHEN "votes" > 0
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT * FROM "sponsorTimes" as c
|
||||||
|
WHERE (c."votes" > "a"."votes" OR c."locked" > "a"."locked") AND
|
||||||
|
c."videoID" = "a"."videoID" AND
|
||||||
|
c."category" = "a"."category" LIMIT 1)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT * FROM "lockCategories" as l
|
||||||
|
WHERE l."videoID" = "a"."videoID" AND l."category" = "a"."category" LIMIT 1)
|
||||||
|
THEN 1 ELSE 0 END) AS "mostUpvotedInLockedVideoSum"
|
||||||
FROM "sponsorTimes" as "a" WHERE "userID" = ?`, [userID, weekAgo, pastDate, userID]) as Promise<ReputationDBResult>;
|
FROM "sponsorTimes" as "a" WHERE "userID" = ?`, [userID, weekAgo, pastDate, userID]) as Promise<ReputationDBResult>;
|
||||||
|
|
||||||
const result = await QueryCacher.get(fetchFromDB, reputationKey(userID));
|
const result = await QueryCacher.get(fetchFromDB, reputationKey(userID));
|
||||||
|
|
||||||
// Grace period
|
return calculateReputationFromMetrics(result);
|
||||||
if (result.totalSubmissions < 5) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downvoteRatio = result.downvotedSubmissions / result.totalSubmissions;
|
|
||||||
if (downvoteRatio > 0.3) {
|
|
||||||
return convertRange(Math.min(downvoteRatio, 0.7), 0.3, 0.7, -0.5, -2.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonSelfDownvoteRatio = result.nonSelfDownvotedSubmissions / result.totalSubmissions;
|
|
||||||
if (nonSelfDownvoteRatio > 0.05) {
|
|
||||||
return convertRange(Math.min(nonSelfDownvoteRatio, 0.4), 0.05, 0.4, -0.5, -2.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.votedSum < 5) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.oldUpvotedSubmissions < 3) {
|
|
||||||
if (result.semiOldUpvotedSubmissions > 3) {
|
|
||||||
return convertRange(Math.min(result.votedSum, 150), 5, 150, 0, 2) + convertRange(Math.min(result.lockedSum ?? 0, 50), 0, 50, 0, 5);
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertRange(Math.min(result.votedSum, 150), 5, 150, 0, 7) + convertRange(Math.min(result.lockedSum ?? 0, 50), 0, 50, 0, 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert a number from one range to another.
|
||||||
function convertRange(value: number, currentMin: number, currentMax: number, targetMin: number, targetMax: number): number {
|
function convertRange(value: number, currentMin: number, currentMax: number, targetMin: number, targetMax: number): number {
|
||||||
const currentRange = currentMax - currentMin;
|
const currentRange = currentMax - currentMin;
|
||||||
const targetRange = targetMax - targetMin;
|
const targetRange = targetMax - targetMin;
|
||||||
return ((value - currentMin) / currentRange) * targetRange + targetMin;
|
return ((value - currentMin) / currentRange) * targetRange + targetMin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateReputationFromMetrics(metrics: ReputationDBResult): number {
|
||||||
|
// Grace period
|
||||||
|
if (metrics.totalSubmissions < 5) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downvoteRatio = metrics.downvotedSubmissions / metrics.totalSubmissions;
|
||||||
|
if (downvoteRatio > 0.3) {
|
||||||
|
return convertRange(Math.min(downvoteRatio, 0.7), 0.3, 0.7, -0.5, -2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonSelfDownvoteRatio = metrics.nonSelfDownvotedSubmissions / metrics.totalSubmissions;
|
||||||
|
if (nonSelfDownvoteRatio > 0.05) {
|
||||||
|
return convertRange(Math.min(nonSelfDownvoteRatio, 0.4), 0.05, 0.4, -0.5, -2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.votedSum < 5) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.oldUpvotedSubmissions < 3) {
|
||||||
|
if (metrics.semiOldUpvotedSubmissions > 3) {
|
||||||
|
return convertRange(Math.min(metrics.votedSum, 150), 5, 150, 0, 2) +
|
||||||
|
convertRange(Math.min((metrics.lockedSum ?? 0) + (metrics.mostUpvotedInLockedVideoSum ?? 0), 50), 0, 50, 0, 5);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertRange(Math.min(metrics.votedSum, 150), 5, 150, 0, 7) +
|
||||||
|
convertRange(Math.min((metrics.lockedSum ?? 0) + (metrics.mostUpvotedInLockedVideoSum ?? 0), 50), 0, 50, 0, 20);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import assert from "assert";
|
|||||||
import { db } from "../../src/databases/databases";
|
import { db } from "../../src/databases/databases";
|
||||||
import { UserID } from "../../src/types/user.model";
|
import { UserID } from "../../src/types/user.model";
|
||||||
import { getHash } from "../../src/utils/getHash";
|
import { getHash } from "../../src/utils/getHash";
|
||||||
import { getReputation } from "../../src/utils/reputation";
|
import { getReputation, calculateReputationFromMetrics } from "../../src/utils/reputation";
|
||||||
|
|
||||||
const userIDLowSubmissions = "reputation-lowsubmissions" as UserID;
|
const userIDLowSubmissions = "reputation-lowsubmissions" as UserID;
|
||||||
const userIDHighDownvotes = "reputation-highdownvotes" as UserID;
|
const userIDHighDownvotes = "reputation-highdownvotes" as UserID;
|
||||||
@@ -12,11 +12,13 @@ const userIDLowSum = "reputation-lowsum" as UserID;
|
|||||||
const userIDHighRepBeforeManualVote = "reputation-oldhighrep" as UserID;
|
const userIDHighRepBeforeManualVote = "reputation-oldhighrep" as UserID;
|
||||||
const userIDHighRep = "reputation-highrep" as UserID;
|
const userIDHighRep = "reputation-highrep" as UserID;
|
||||||
const userIDHighRepAndLocked = "reputation-highlockedrep" as UserID;
|
const userIDHighRepAndLocked = "reputation-highlockedrep" as UserID;
|
||||||
|
const userIDHaveMostUpvotedInLockedVideo = "reputation-mostupvotedaslocked" as UserID;
|
||||||
|
|
||||||
describe("reputation", () => {
|
describe("reputation", () => {
|
||||||
before(async function() {
|
before(async function() {
|
||||||
this.timeout(5000); // this preparation takes longer then usual
|
this.timeout(5000); // this preparation takes longer then usual
|
||||||
const videoID = "reputation-videoID";
|
const videoID = "reputation-videoID";
|
||||||
|
const videoID2 = "reputation-videoID-2";
|
||||||
|
|
||||||
const sponsorTimesInsertQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "videoDuration", "hidden", "shadowHidden", "hashedVideoID") VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
const sponsorTimesInsertQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "service", "videoDuration", "hidden", "shadowHidden", "hashedVideoID") VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||||
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 2, 0, "reputation-0-uuid-0", getHash(userIDLowSubmissions), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 2, 0, "reputation-0-uuid-0", getHash(userIDLowSubmissions), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
@@ -87,6 +89,28 @@ describe("reputation", () => {
|
|||||||
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, -1, 0, "reputation-6-uuid-5", getHash(userIDHighRepAndLocked), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, -1, 0, "reputation-6-uuid-5", getHash(userIDHighRepAndLocked), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 0, 0, "reputation-6-uuid-6", getHash(userIDHighRepAndLocked), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 0, 0, "reputation-6-uuid-6", getHash(userIDHighRepAndLocked), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 0, 0, "reputation-6-uuid-7", getHash(userIDHighRepAndLocked), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 0, 0, "reputation-6-uuid-7", getHash(userIDHighRepAndLocked), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
|
||||||
|
//Record has most upvoted
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 5, 0, "reputation-7-uuid-0", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 101, 0, "reputation-7-uuid-1", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "intro", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID2, 1, 11, 5, 0, "reputation-7-uuid-8", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID2, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID2, 1, 11, 0, 0, "reputation-7-uuid-9", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID2, 1)]);
|
||||||
|
// other segments
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 2, 0, "reputation-7-uuid-2", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 2, 0, "reputation-7-uuid-3", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 2, 0, "reputation-7-uuid-4", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, -1, 0, "reputation-7-uuid-5", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 0, 0, "reputation-7-uuid-6", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", sponsorTimesInsertQuery, [videoID, 1, 11, 0, 0, "reputation-7-uuid-7", getHash(userIDHaveMostUpvotedInLockedVideo), 1606240000000, 50, "sponsor", "YouTube", 100, 0, 0, getHash(videoID, 1)]);
|
||||||
|
|
||||||
|
// lock video
|
||||||
|
const insertVipUserQuery = 'INSERT INTO "vipUsers" ("userID") VALUES (?)';
|
||||||
|
await db.prepare("run", insertVipUserQuery, [getHash("VIPUser-getLockCategories")]);
|
||||||
|
|
||||||
|
const insertLockCategoryQuery = 'INSERT INTO "lockCategories" ("userID", "videoID", "category", "hashedVideoID") VALUES (?, ?, ?, ?)';
|
||||||
|
await db.prepare("run", insertLockCategoryQuery, [getHash("VIPUser-getLockCategories"), videoID, "sponsor", getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", insertLockCategoryQuery, [getHash("VIPUser-getLockCategories"), videoID, "intro", getHash(videoID, 1)]);
|
||||||
|
await db.prepare("run", insertLockCategoryQuery, [getHash("VIPUser-getLockCategories"), videoID2, "sponsor", getHash(videoID2, 1)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("user in grace period", async () => {
|
it("user in grace period", async () => {
|
||||||
@@ -94,10 +118,33 @@ describe("reputation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("user with high downvote ratio", async () => {
|
it("user with high downvote ratio", async () => {
|
||||||
|
const metrics = {
|
||||||
|
totalSubmissions: 8,
|
||||||
|
downvotedSubmissions: 5,
|
||||||
|
nonSelfDownvotedSubmissions: 0,
|
||||||
|
votedSum: -7,
|
||||||
|
lockedSum: 0,
|
||||||
|
semiOldUpvotedSubmissions: 1,
|
||||||
|
oldUpvotedSubmissions: 1,
|
||||||
|
mostUpvotedInLockedVideoSum: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(await getReputation(getHash(userIDHighDownvotes)), calculateReputationFromMetrics(metrics));
|
||||||
assert.strictEqual(await getReputation(getHash(userIDHighDownvotes)), -2.125);
|
assert.strictEqual(await getReputation(getHash(userIDHighDownvotes)), -2.125);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("user with high non self downvote ratio", async () => {
|
it("user with high non self downvote ratio", async () => {
|
||||||
|
const metrics = {
|
||||||
|
totalSubmissions: 8,
|
||||||
|
downvotedSubmissions: 2,
|
||||||
|
nonSelfDownvotedSubmissions: 2,
|
||||||
|
votedSum: -1,
|
||||||
|
lockedSum: 0,
|
||||||
|
semiOldUpvotedSubmissions: 1,
|
||||||
|
oldUpvotedSubmissions: 1,
|
||||||
|
mostUpvotedInLockedVideoSum: 0
|
||||||
|
};
|
||||||
|
assert.strictEqual(await getReputation(getHash(userIDHighNonSelfDownvotes)), calculateReputationFromMetrics(metrics));
|
||||||
assert.strictEqual(await getReputation(getHash(userIDHighNonSelfDownvotes)), -1.6428571428571428);
|
assert.strictEqual(await getReputation(getHash(userIDHighNonSelfDownvotes)), -1.6428571428571428);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,11 +161,48 @@ describe("reputation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("user with high reputation", async () => {
|
it("user with high reputation", async () => {
|
||||||
|
const metrics = {
|
||||||
|
totalSubmissions: 8,
|
||||||
|
downvotedSubmissions: 1,
|
||||||
|
nonSelfDownvotedSubmissions: 0,
|
||||||
|
votedSum: 9,
|
||||||
|
lockedSum: 0,
|
||||||
|
semiOldUpvotedSubmissions: 5,
|
||||||
|
oldUpvotedSubmissions: 5,
|
||||||
|
mostUpvotedInLockedVideoSum: 0
|
||||||
|
};
|
||||||
|
assert.strictEqual(await getReputation(getHash(userIDHighRep)), calculateReputationFromMetrics(metrics));
|
||||||
assert.strictEqual(await getReputation(getHash(userIDHighRep)), 0.19310344827586207);
|
assert.strictEqual(await getReputation(getHash(userIDHighRep)), 0.19310344827586207);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("user with high reputation and locked segments", async () => {
|
it("user with high reputation and locked segments", async () => {
|
||||||
|
const metrics = {
|
||||||
|
totalSubmissions: 8,
|
||||||
|
downvotedSubmissions: 1,
|
||||||
|
nonSelfDownvotedSubmissions: 0,
|
||||||
|
votedSum: 9,
|
||||||
|
lockedSum: 4,
|
||||||
|
semiOldUpvotedSubmissions: 5,
|
||||||
|
oldUpvotedSubmissions: 5,
|
||||||
|
mostUpvotedInLockedVideoSum: 0
|
||||||
|
};
|
||||||
|
assert.strictEqual(await getReputation(getHash(userIDHighRepAndLocked)), calculateReputationFromMetrics(metrics));
|
||||||
assert.strictEqual(await getReputation(getHash(userIDHighRepAndLocked)), 1.793103448275862);
|
assert.strictEqual(await getReputation(getHash(userIDHighRepAndLocked)), 1.793103448275862);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("user with most upvoted segments in locked video", async () => {
|
||||||
|
const metrics = {
|
||||||
|
totalSubmissions: 10,
|
||||||
|
downvotedSubmissions: 1,
|
||||||
|
nonSelfDownvotedSubmissions: 0,
|
||||||
|
votedSum: 116,
|
||||||
|
lockedSum: 0,
|
||||||
|
semiOldUpvotedSubmissions: 6,
|
||||||
|
oldUpvotedSubmissions: 6,
|
||||||
|
mostUpvotedInLockedVideoSum: 2
|
||||||
|
};
|
||||||
|
assert.strictEqual(await getReputation(getHash(userIDHaveMostUpvotedInLockedVideo)), calculateReputationFromMetrics(metrics));
|
||||||
|
assert.strictEqual(await getReputation(getHash(userIDHaveMostUpvotedInLockedVideo)), 6.158620689655172);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user