Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into fix-eslint

This commit is contained in:
Michael C
2021-07-04 15:23:00 -04:00
10 changed files with 279 additions and 75 deletions

View File

@@ -3,7 +3,7 @@ import { config } from '../config';
import { db, privateDB } from '../databases/databases';
import { skipSegmentsHashKey, skipSegmentsKey } from '../utils/redisKeys';
import { SBRecord } from '../types/lib.model';
import { Category, CategoryActionType, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
import { Category, CategoryActionType, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, SegmentUUID, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
import { getCategoryActionType } from '../utils/categoryInfo';
import { getHash } from '../utils/getHash';
import { getIP } from '../utils/getIP';
@@ -14,7 +14,7 @@ import { getReputation } from '../utils/reputation';
async function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Promise<Segment[]> {
const shouldFilter: boolean[] = await Promise.all(segments.map(async (segment) => {
if (segment.votes < -1) {
if (segment.votes < -1 && !segment.required) {
return false; //too untrustworthy, just ignore it
}
@@ -50,7 +50,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
}));
}
async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], service: Service): Promise<Segment[]> {
async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
const segments: Segment[] = [];
@@ -61,6 +61,8 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
const segmentsByCategory: SBRecord<Category, DBSegment[]> = (await getSegmentsFromDBByVideoID(videoID, service))
.filter((segment: DBSegment) => categories.includes(segment?.category))
.reduce((acc: SBRecord<Category, DBSegment[]>, segment: DBSegment) => {
if (requiredSegments.includes(segment.UUID)) segment.required = true;
acc[segment.category] = acc[segment.category] || [];
acc[segment.category].push(segment);
@@ -80,7 +82,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
}
}
async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[], requiredSegments: SegmentUUID[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
const segments: SBRecord<VideoID, VideoData> = {};
@@ -97,8 +99,9 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
hash: segment.hashedVideoID,
segmentPerCategory: {},
};
const videoCategories = acc[segment.videoID].segmentPerCategory;
if (requiredSegments.includes(segment.UUID)) segment.required = true;
const videoCategories = acc[segment.videoID].segmentPerCategory;
videoCategories[segment.category] = videoCategories[segment.category] || [];
videoCategories[segment.category].push(segment);
@@ -214,7 +217,7 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created
for (const segment of segments) {
if (segment.startTime > cursor) {
currentGroup = {segments: [], votes: 0, reputation: 0, locked: false};
currentGroup = {segments: [], votes: 0, reputation: 0, locked: false, required: false};
overlappingSegmentsGroups.push(currentGroup);
}
@@ -233,11 +236,18 @@ async function chooseSegments(segments: DBSegment[], max: number): Promise<DBSeg
currentGroup.locked = true;
}
if (segment.required) {
currentGroup.required = true;
}
cursor = Math.max(cursor, segment.endTime);
}
overlappingSegmentsGroups.forEach((group) => {
if (group.locked) {
if (group.required) {
// Required beats locked
group.segments = group.segments.filter((segment) => segment.required);
} else if (group.locked) {
group.segments = group.segments.filter((segment) => segment.locked);
}
@@ -277,12 +287,24 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
return false;
}
const requiredSegments: SegmentUUID[] = req.query.requiredSegments
? JSON.parse(req.query.requiredSegments as string)
: req.query.requiredSegment
? Array.isArray(req.query.requiredSegment)
? req.query.requiredSegment
: [req.query.requiredSegment]
: [];
if (!Array.isArray(requiredSegments)) {
res.status(400).send("requiredSegments parameter does not match format requirements.");
return false;
}
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube;
if (!Object.values(Service).some((val) => val == service)) {
service = Service.YouTube;
}
const segments = await getSegmentsByVideoID(req, videoID, categories, service);
const segments = await getSegmentsByVideoID(req, videoID, categories, requiredSegments, service);
if (segments === null || segments === undefined) {
res.sendStatus(500);

View File

@@ -1,7 +1,7 @@
import {hashPrefixTester} from '../utils/hashPrefixTester';
import {getSegmentsByHash} from './getSkipSegments';
import {Request, Response} from 'express';
import { Category, Service, VideoIDHash } from '../types/segments.model';
import { Category, SegmentUUID, Service, VideoIDHash } from '../types/segments.model';
export async function getSkipSegmentsByHash(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
@@ -15,16 +15,33 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
categories = req.query.categories
? JSON.parse(req.query.categories as string)
: req.query.category
? [req.query.category]
: ["sponsor"];
? Array.isArray(req.query.category)
? req.query.category
: [req.query.category]
: ['sponsor'];
if (!Array.isArray(categories)) {
return res.status(400).send("Categories parameter does not match format requirements.");
}
}
catch(error) {
} catch(error) {
return res.status(400).send("Bad parameter: categories (invalid JSON)");
}
let requiredSegments: SegmentUUID[] = [];
try {
requiredSegments = req.query.requiredSegments
? JSON.parse(req.query.requiredSegments as string)
: req.query.requiredSegment
? Array.isArray(req.query.requiredSegment)
? req.query.requiredSegment
: [req.query.requiredSegment]
: [];
if (!Array.isArray(requiredSegments)) {
return res.status(400).send("requiredSegments parameter does not match format requirements.");
}
} catch(error) {
return res.status(400).send("Bad parameter: requiredSegments (invalid JSON)");
}
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube;
if (!Object.values(Service).some((val) => val == service)) {
service = Service.YouTube;
@@ -34,7 +51,7 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
categories = categories.filter((item: any) => typeof item === "string");
// Get all video id's that match hash prefix
const segments = await getSegmentsByHash(req, hashPrefix, categories, service);
const segments = await getSegmentsByHash(req, hashPrefix, categories, requiredSegments, service);
if (!segments) return res.status(404).json([]);

View File

@@ -266,6 +266,34 @@ async function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promi
}
}
async function checkUserActiveWarning(userID: string): Promise<{ pass: boolean; errorMessage: string; }> {
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const warnings = await db.prepare('all',
`SELECT "reason"
FROM warnings
WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1
ORDER BY "issueTime" DESC
LIMIT ?`,
[
userID,
Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR)),
config.maxNumberOfActiveWarnings
],
) as {reason: string}[];
if (warnings?.length >= config.maxNumberOfActiveWarnings) {
const defaultMessage = 'Submission 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?';
return {
pass: false,
errorMessage: warnings[0]?.reason?.length > 0 ? warnings[0].reason : defaultMessage
};
}
return {pass: true, errorMessage: ''};
}
function proxySubmission(req: Request) {
fetch(config.proxySubmission + '/api/skipSegments?userID=' + req.query.userID + '&videoID=' + req.query.videoID, {
method: 'POST',
@@ -321,17 +349,9 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
//hash the userID
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
const hashedIP = getHash(getIP(req) + config.globalSalt);
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const warningsCount = (await db.prepare('get', `SELECT count(*) as count FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
[userID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
)).count;
if (warningsCount >= config.maxNumberOfActiveWarnings) {
return res.status(403).send('Submission 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?');
const warningResult: {pass: boolean, errorMessage: string} = await checkUserActiveWarning(userID);
if (!warningResult.pass) {
return res.status(403).send(warningResult.errorMessage);
}
let lockedCategoryList = (await db.prepare('all', 'SELECT category from "lockCategories" where "videoID" = ?', [videoID])).map((list: any) => list.category );
@@ -341,9 +361,15 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
const decreaseVotes = 0;
const previousSubmissions = await db.prepare('all', `SELECT "videoDuration", "UUID" FROM "sponsorTimes" WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0
AND "shadowHidden" = 0 AND "votes" >= 0 AND "videoDuration" != 0`, [videoID, service]) as
{videoDuration: VideoDuration, UUID: SegmentUUID}[];
const previousSubmissions = await db.prepare('all',
`SELECT "videoDuration", "UUID"
FROM "sponsorTimes"
WHERE "videoID" = ? AND "service" = ? AND
"hidden" = 0 AND "shadowHidden" = 0 AND
"votes" >= 0 AND "videoDuration" != 0`,
[videoID, service]
) as {videoDuration: VideoDuration, UUID: SegmentUUID}[];
// 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 && previousSubmissions.length > 0 && !previousSubmissions.some((e) => Math.abs(videoDuration - e.videoDuration) < 2);
@@ -436,6 +462,9 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
const UUIDs = [];
const newSegments = [];
//hash the ip 5000 times so no one can get it from the database
const hashedIP = getHash(getIP(req) + config.globalSalt);
try {
//get current time
const timeSubmitted = Date.now();