Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into feat/faster-segments

# Conflicts:
#	src/routes/getSkipSegments.ts
#	src/routes/getSkipSegmentsByHash.ts
#	test/cases/getSegmentsByHash.js
This commit is contained in:
Ajay Ramachandran
2020-12-24 21:27:08 -05:00
122 changed files with 5390 additions and 4643 deletions

View File

@@ -1,19 +1,15 @@
var fs = require('fs');
var config = require('../config.js');
import {getHash} from '../utils/getHash';
import {db} from '../databases/databases';
import {config} from '../config';
import {Request, Response} from 'express';
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
export async function addUserAsVIP(req: Request, res: Response) {
const userID = req.query.userID as string;
let adminUserIDInput = req.query.adminUserID as string;
module.exports = async function addUserAsVIP (req, res) {
let userID = req.query.userID;
let adminUserIDInput = req.query.adminUserID;
let enabled = req.query.enabled;
if (enabled === undefined){
enabled = true;
} else {
enabled = enabled === "true";
}
const enabled = req.query.enabled === undefined
? false
: req.query.enabled === 'true';
if (userID == undefined || adminUserIDInput == undefined) {
//invalid request
@@ -31,7 +27,7 @@ module.exports = async function addUserAsVIP (req, res) {
}
//check to see if this user is already a vip
let row = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]);
const row = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]);
if (enabled && row.userCount == 0) {
//add them to the vip list
@@ -42,4 +38,4 @@ module.exports = async function addUserAsVIP (req, res) {
}
res.sendStatus(200);
}
}

View File

@@ -1,43 +1,43 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
import {Request, Response} from 'express';
import {isUserVIP} from '../utils/isUserVIP';
import {getHash} from '../utils/getHash';
import {db} from '../databases/databases';
module.exports = (req, res) => {
export function deleteNoSegments(req: Request, res: Response) {
// Collect user input data
let videoID = req.body.videoID;
const videoID = req.body.videoID;
let userID = req.body.userID;
let categories = req.body.categories;
const categories = req.body.categories;
// Check input data is valid
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
) {
res.status(400).json({
message: 'Bad Format'
message: 'Bad Format',
});
return;
}
// Check if user is VIP
userID = getHash(userID);
let userIsVIP = isUserVIP(userID);
const userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to mark videos.'
message: 'Must be a VIP to mark videos.',
});
return;
}
db.prepare("all", 'SELECT * FROM noSegments WHERE videoID = ?', [videoID]).filter((entry) => {
db.prepare("all", 'SELECT * FROM noSegments WHERE videoID = ?', [videoID]).filter((entry: any) => {
return (categories.indexOf(entry.category) !== -1);
}).forEach((entry) => {
}).forEach((entry: any) => {
db.prepare('run', 'DELETE FROM noSegments WHERE videoID = ? AND category = ?', [videoID, entry.category]);
});
res.status(200).json({message: 'Removed no segments entrys for video ' + videoID});
};
}

View File

@@ -1,12 +0,0 @@
var db = require('../databases/databases.js').db;
module.exports = function getDaysSavedFormatted (req, res) {
let row = db.prepare('get', "SELECT SUM((endTime - startTime) / 60 / 60 / 24 * views) as daysSaved from sponsorTimes where shadowHidden != 1", []);
if (row !== undefined) {
//send this result
res.send({
daysSaved: row.daysSaved.toFixed(2)
});
}
}

View File

@@ -0,0 +1,13 @@
import {db} from '../databases/databases';
import {Request, Response} from 'express';
export function getDaysSavedFormatted(req: Request, res: Response) {
let row = db.prepare('get', "SELECT SUM((endTime - startTime) / 60 / 60 / 24 * views) as daysSaved from sponsorTimes where shadowHidden != 1", []);
if (row !== undefined) {
//send this result
res.send({
daysSaved: row.daysSaved.toFixed(2),
});
}
}

View File

@@ -1,11 +1,10 @@
var db = require('../databases/databases.js').db;
import {Logger} from '../utils/logger';
import {getHash} from '../utils/getHash';
import {isUserVIP} from '../utils/isUserVIP';
import {Request, Response} from 'express';
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
const isUserVIP = require('../utils/isUserVIP.js');
module.exports = (req, res) => {
let userID = req.query.userID;
export function getIsUserVIP(req: Request, res: Response): void {
let userID = req.query.userID as string;
if (userID == undefined) {
//invalid request
@@ -20,10 +19,10 @@ module.exports = (req, res) => {
let vipState = isUserVIP(userID);
res.status(200).json({
hashedUserID: userID,
vip: vipState
vip: vipState,
});
} catch (err) {
logger.error(err);
Logger.error(err);
res.sendStatus(500);
return;

View File

@@ -1,32 +0,0 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
module.exports = function getSavedTimeForUser (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare("get", "SELECT SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE userID = ? AND votes > -1 AND shadowHidden != 1 ", [userID]);
if (row.minutesSaved != null) {
res.send({
timeSaved: row.minutesSaved
});
} else {
res.sendStatus(404);
}
} catch (err) {
console.log(err);
res.sendStatus(500);
return;
}
}

View File

@@ -0,0 +1,33 @@
import {db} from '../databases/databases';
import {Request, Response} from 'express';
import {getHash} from '../utils/getHash';
export function getSavedTimeForUser(req: Request, res: Response) {
let userID = req.query.userID as string;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare("get", "SELECT SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE userID = ? AND votes > -1 AND shadowHidden != 1 ", [userID]);
if (row.minutesSaved != null) {
res.send({
timeSaved: row.minutesSaved,
});
} else {
res.sendStatus(404);
}
} catch (err) {
console.log(err);
res.sendStatus(500);
return;
}
}

View File

@@ -1,14 +1,13 @@
var config = require('../config.js');
import { Request, Response } from 'express';
import { config } from '../config';
import { db, privateDB } from '../databases/databases';
import { Category, DBSegment, OverlappingSegmentGroup, Segment, SegmentCache, VideoData, VideoID, VideoIDHash, VotableObject } from "../types/segments.model";
import { getHash } from '../utils/getHash';
import { getIP } from '../utils/getIP';
import { Logger } from '../utils/logger';
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var logger = require('../utils/logger.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
function prepareCategorySegments(req, videoID, category, segments, cache = {shadowHiddenSegments: {}}) {
function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[], cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Segment[] {
const filteredSegments = segments.filter((segment) => {
if (segment.votes < -1) {
return false; //too untrustworthy, just ignore it
@@ -20,12 +19,12 @@ function prepareCategorySegments(req, videoID, category, segments, cache = {shad
return true;
}
if (cache.shadowHiddenSegments[videoID] === undefined) {
cache.shadowHiddenSegments[videoID] = privateDB.prepare('all', 'SELECT hashedIP FROM sponsorTimes WHERE videoID = ?', [videoID]);
if (cache.shadowHiddenSegmentIPs[videoID] === undefined) {
cache.shadowHiddenSegmentIPs[videoID] = privateDB.prepare('all', 'SELECT hashedIP FROM sponsorTimes WHERE videoID = ?', [videoID]);
}
//if this isn't their ip, don't send it to them
return cache.shadowHiddenSegments[videoID].some((shadowHiddenSegment) => {
return cache.shadowHiddenSegmentIPs[videoID].some((shadowHiddenSegment) => {
if (cache.userHashedIP === undefined) {
//hash the IP only if it's strictly necessary
cache.userHashedIP = getHash(getIP(req) + config.globalSalt);
@@ -38,21 +37,21 @@ function prepareCategorySegments(req, videoID, category, segments, cache = {shad
return chooseSegments(filteredSegments).map((chosenSegment) => ({
category,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
UUID: chosenSegment.UUID
}));
}
function getSegmentsByVideoID(req, videoID, categories) {
const cache = {};
const segments = [];
function getSegmentsByVideoID(req: Request, videoID: string, categories: Category[]): Segment[] {
const cache: SegmentCache = {};
const segments: Segment[] = [];
try {
const segmentsByCategory = db
const segmentsByCategory: Record<Category, DBSegment[]> = db
.prepare(
'all',
`SELECT startTime, endTime, votes, UUID, category, shadowHidden FROM sponsorTimes WHERE videoID = ? AND category IN (${Array(categories.length).fill('?').join()}) ORDER BY startTime`,
[videoID, categories]
).reduce((acc, segment) => {
).reduce((acc: Record<Category, DBSegment[]>, segment: DBSegment) => {
acc[segment.category] = acc[segment.category] || [];
acc[segment.category].push(segment);
@@ -66,28 +65,30 @@ function getSegmentsByVideoID(req, videoID, categories) {
return segments;
} catch (err) {
if (err) {
logger.error(err);
Logger.error(err);
return null;
}
}
}
function getSegmentsByHash(req, hashedVideoIDPrefix, categories) {
const cache = {};
const segments = {};
function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[]): Record<VideoID, VideoData> {
const cache: SegmentCache = {};
const segments: Record<VideoID, VideoData> = {};
try {
const allSegments = db
type SegmentWithHashPerVideoID = Record<VideoID, {hash: VideoIDHash, segmentPerCategory: Record<Category, DBSegment[]>}>;
const segmentPerVideoID: SegmentWithHashPerVideoID = db
.prepare(
'all',
`SELECT videoID, startTime, endTime, votes, UUID, category, shadowHidden, hashedVideoID FROM sponsorTimes WHERE hashedVideoID LIKE ? AND category IN (${Array(categories.length).fill('?').join()}) ORDER BY startTime`,
[hashedVideoIDPrefix + '%', categories]
).reduce((acc, segment) => {
).reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
acc[segment.videoID] = acc[segment.videoID] || {
hash: segment.hashedVideoID,
categories: {},
segmentPerCategory: {},
};
const videoCategories = acc[segment.videoID].categories;
const videoCategories = acc[segment.videoID].segmentPerCategory;
videoCategories[segment.category] = videoCategories[segment.category] || [];
videoCategories[segment.category].push(segment);
@@ -95,21 +96,21 @@ function getSegmentsByHash(req, hashedVideoIDPrefix, categories) {
return acc;
}, {});
for (const [videoID, videoData] of Object.entries(allSegments)) {
for (const [videoID, videoData] of Object.entries(segmentPerVideoID)) {
segments[videoID] = {
hash: videoData.hash,
segments: [],
};
for (const [category, categorySegments] of Object.entries(videoData.categories)) {
segments[videoID].segments.push(...prepareCategorySegments(req, videoID, category, categorySegments, cache));
for (const [category, segmentPerCategory] of Object.entries(videoData.segmentPerCategory)) {
segments[videoID].segments.push(...prepareCategorySegments(req, videoID, category, segmentPerCategory, cache));
}
}
return segments;
} catch (err) {
if (err) {
logger.error(err);
Logger.error(err);
return null;
}
}
@@ -118,22 +119,25 @@ function getSegmentsByHash(req, hashedVideoIDPrefix, categories) {
//gets a weighted random choice from the choices array based on their `votes` property.
//amountOfChoices specifies the maximum amount of choices to return, 1 or more.
//choices are unique
function getWeightedRandomChoice(choices, amountOfChoices) {
function getWeightedRandomChoice<T extends VotableObject>(choices: T[], amountOfChoices: number): T[] {
//trivial case: no need to go through the whole process
if (amountOfChoices >= choices.length) {
return choices;
}
type TWithWeight = T & {
weight: number
}
//assign a weight to each choice
let totalWeight = 0;
choices = choices.map(choice => {
let choicesWithWeights: TWithWeight[] = choices.map(choice => {
//The 3 makes -2 the minimum votes before being ignored completely
//https://www.desmos.com/calculator/c1duhfrmts
//this can be changed if this system increases in popularity.
const weight = Math.exp((choice.votes + 3), 0.85);
const weight = Math.exp((choice.votes + 3));
totalWeight += weight;
return { ...choice, weight };
return {...choice, weight};
});
//iterate and find amountOfChoices choices
@@ -141,16 +145,16 @@ function getWeightedRandomChoice(choices, amountOfChoices) {
while (amountOfChoices-- > 0) {
//weighted random draw of one element of choices
const randomNumber = Math.random() * totalWeight;
let stackWeight = choices[0].weight;
let stackWeight = choicesWithWeights[0].weight;
let i = 0;
while (stackWeight < randomNumber) {
stackWeight += choices[++i].weight;
stackWeight += choicesWithWeights[++i].weight;
}
//add it to the chosen ones and remove it from the choices before the next iteration
chosen.push(choices[i]);
totalWeight -= choices[i].weight;
choices.splice(i, 1);
chosen.push(choicesWithWeights[i]);
totalWeight -= choicesWithWeights[i].weight;
choicesWithWeights.splice(i, 1);
}
return chosen;
@@ -160,19 +164,19 @@ function getWeightedRandomChoice(choices, amountOfChoices) {
//Only one similar time will be returned, randomly generated based on the sqrt of votes.
//This allows new less voted items to still sometimes appear to give them a chance at getting votes.
//Segments with less than -1 votes are already ignored before this function is called
function chooseSegments(segments) {
function chooseSegments(segments: DBSegment[]): DBSegment[] {
//Create groups of segments that are similar to eachother
//Segments must be sorted by their startTime so that we can build groups chronologically:
//1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group
//2. If a segment starts after the end of the currentGroup (> cursor), no other segment will ever fall
// inside that group (because they're sorted) so we can create a new one
const similarSegmentsGroups = [];
let currentGroup;
const overlappingSegmentsGroups: OverlappingSegmentGroup[] = [];
let currentGroup: OverlappingSegmentGroup;
let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created
segments.forEach(segment => {
if (segment.startTime > cursor) {
currentGroup = { segments: [], votes: 0 };
similarSegmentsGroups.push(currentGroup);
currentGroup = {segments: [], votes: 0};
overlappingSegmentsGroups.push(currentGroup);
}
currentGroup.segments.push(segment);
@@ -185,9 +189,9 @@ function chooseSegments(segments) {
});
//if there are too many groups, find the best 8
return getWeightedRandomChoice(similarSegmentsGroups, 32).map(
return getWeightedRandomChoice(overlappingSegmentsGroups, 32).map(
//randomly choose 1 good segment per group and return them
group => getWeightedRandomChoice(group.segments, 1)[0]
group => getWeightedRandomChoice(group.segments, 1)[0],
);
}
@@ -201,15 +205,15 @@ function chooseSegments(segments) {
*
* @returns
*/
function handleGetSegments(req, res) {
const videoID = req.query.videoID;
function handleGetSegments(req: Request, res: Response) {
const videoID = req.query.videoID as string;
// Default to sponsor
// If using params instead of JSON, only one category can be pulled
const categories = req.query.categories
? JSON.parse(req.query.categories)
? JSON.parse(req.query.categories as string)
: req.query.category
? [req.query.category]
: ['sponsor'];
? [req.query.category]
: ['sponsor'];
const segments = getSegmentsByVideoID(req, videoID, categories);
@@ -226,16 +230,19 @@ function handleGetSegments(req, res) {
return segments;
}
module.exports = {
handleGetSegments,
function endpoint(req: Request, res: Response): void {
let segments = handleGetSegments(req, res);
if (segments) {
//send result
res.send(segments);
}
}
export {
getSegmentsByVideoID,
getSegmentsByHash,
endpoint: function (req, res) {
let segments = handleGetSegments(req, res);
if (segments) {
//send result
res.send(segments);
}
},
endpoint,
handleGetSegments
};

View File

@@ -1,33 +0,0 @@
const hashPrefixTester = require('../utils/hashPrefixTester.js');
const getSegments = require('./getSkipSegments.js').getSegmentsByHash;
const databases = require('../databases/databases.js');
const logger = require('../utils/logger.js');
const db = databases.db;
module.exports = async function (req, res) {
let hashPrefix = req.params.prefix;
if (!hashPrefixTester(req.params.prefix)) {
res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
return;
}
const categories = req.query.categories
? JSON.parse(req.query.categories)
: req.query.category
? [req.query.category]
: ['sponsor'];
// Get all video id's that match hash prefix
const segments = getSegments(req, hashPrefix, categories);
if (!segments) return res.status(404).json([]);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
hash: data.hash,
segments: data.segments,
}));
res.status(output.length === 0 ? 404 : 200).json(output);
}

View File

@@ -0,0 +1,31 @@
import {hashPrefixTester} from '../utils/hashPrefixTester';
import {getSegmentsByHash} from './getSkipSegments';
import {Request, Response} from 'express';
import { Category, VideoIDHash } from '../types/segments.model';
export async function getSkipSegmentsByHash(req: Request, res: Response) {
let hashPrefix: VideoIDHash = req.params.prefix;
if (!hashPrefixTester(req.params.prefix)) {
res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
return;
}
const categories: Category[] = req.query.categories
? JSON.parse(req.query.categories as string)
: req.query.category
? [req.query.category]
: ['sponsor'];
// Get all video id's that match hash prefix
const segments = getSegmentsByHash(req, hashPrefix, categories);
if (!segments) return res.status(404).json([]);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
hash: data.hash,
segments: data.segments,
}));
res.status(output.length === 0 ? 404 : 200).json(output);
}

View File

@@ -1,92 +0,0 @@
var db = require('../databases/databases.js').db;
const logger = require('../utils/logger.js');
const createMemoryCache = require('../utils/createMemoryCache.js');
const config = require('../config.js');
const MILLISECONDS_IN_MINUTE = 60000;
const getTopUsersWithCache = createMemoryCache(generateTopUsersStats, config.getTopUsersCacheTimeMinutes * MILLISECONDS_IN_MINUTE);
function generateTopUsersStats(sortBy, categoryStatsEnabled = false) {
return new Promise((resolve, reject) => {
const userNames = [];
const viewCounts = [];
const totalSubmissions = [];
const minutesSaved = [];
const categoryStats = categoryStatsEnabled ? [] : undefined;
let additionalFields = '';
if (categoryStatsEnabled) {
additionalFields += "SUM(CASE WHEN category = 'sponsor' THEN 1 ELSE 0 END) as categorySponsor, " +
"SUM(CASE WHEN category = 'intro' THEN 1 ELSE 0 END) as categorySumIntro, " +
"SUM(CASE WHEN category = 'outro' THEN 1 ELSE 0 END) as categorySumOutro, " +
"SUM(CASE WHEN category = 'interaction' THEN 1 ELSE 0 END) as categorySumInteraction, " +
"SUM(CASE WHEN category = 'selfpromo' THEN 1 ELSE 0 END) as categorySelfpromo, " +
"SUM(CASE WHEN category = 'music_offtopic' THEN 1 ELSE 0 END) as categoryMusicOfftopic, ";
}
const rows = db.prepare('all', "SELECT COUNT(*) as totalSubmissions, SUM(views) as viewCount," +
"SUM((sponsorTimes.endTime - sponsorTimes.startTime) / 60 * sponsorTimes.views) as minutesSaved, " +
"SUM(votes) as userVotes, " +
additionalFields +
"IFNULL(userNames.userName, sponsorTimes.userID) as userName FROM sponsorTimes LEFT JOIN userNames ON sponsorTimes.userID=userNames.userID " +
"LEFT JOIN privateDB.shadowBannedUsers ON sponsorTimes.userID=privateDB.shadowBannedUsers.userID " +
"WHERE sponsorTimes.votes > -1 AND sponsorTimes.shadowHidden != 1 AND privateDB.shadowBannedUsers.userID IS NULL " +
"GROUP BY IFNULL(userName, sponsorTimes.userID) HAVING userVotes > 20 " +
"ORDER BY " + sortBy + " DESC LIMIT 100", []);
for (let i = 0; i < rows.length; i++) {
userNames[i] = rows[i].userName;
viewCounts[i] = rows[i].viewCount;
totalSubmissions[i] = rows[i].totalSubmissions;
minutesSaved[i] = rows[i].minutesSaved;
if (categoryStatsEnabled) {
categoryStats[i] = [
rows[i].categorySponsor,
rows[i].categorySumIntro,
rows[i].categorySumOutro,
rows[i].categorySumInteraction,
rows[i].categorySelfpromo,
rows[i].categoryMusicOfftopic,
];
}
}
resolve({
userNames,
viewCounts,
totalSubmissions,
minutesSaved,
categoryStats
});
});
}
module.exports = async function getTopUsers (req, res) {
let sortType = req.query.sortType;
let categoryStatsEnabled = req.query.categoryStats;
if (sortType == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//setup which sort type to use
let sortBy = '';
if (sortType == 0) {
sortBy = 'minutesSaved';
} else if (sortType == 1) {
sortBy = 'viewCount';
} else if (sortType == 2) {
sortBy = 'totalSubmissions';
} else {
//invalid request
return res.sendStatus(400);
}
const stats = await getTopUsersWithCache(sortBy, categoryStatsEnabled);
//send this result
res.send(stats);
}

92
src/routes/getTopUsers.ts Normal file
View File

@@ -0,0 +1,92 @@
import {db} from '../databases/databases';
import {createMemoryCache} from '../utils/createMemoryCache';
import {config} from '../config';
import {Request, Response} from 'express';
const MILLISECONDS_IN_MINUTE = 60000;
const getTopUsersWithCache = createMemoryCache(generateTopUsersStats, config.getTopUsersCacheTimeMinutes * MILLISECONDS_IN_MINUTE);
function generateTopUsersStats(sortBy: string, categoryStatsEnabled: boolean = false) {
return new Promise((resolve) => {
const userNames = [];
const viewCounts = [];
const totalSubmissions = [];
const minutesSaved = [];
const categoryStats: any[] = categoryStatsEnabled ? [] : undefined;
let additionalFields = '';
if (categoryStatsEnabled) {
additionalFields += "SUM(CASE WHEN category = 'sponsor' THEN 1 ELSE 0 END) as categorySponsor, " +
"SUM(CASE WHEN category = 'intro' THEN 1 ELSE 0 END) as categorySumIntro, " +
"SUM(CASE WHEN category = 'outro' THEN 1 ELSE 0 END) as categorySumOutro, " +
"SUM(CASE WHEN category = 'interaction' THEN 1 ELSE 0 END) as categorySumInteraction, " +
"SUM(CASE WHEN category = 'selfpromo' THEN 1 ELSE 0 END) as categorySelfpromo, " +
"SUM(CASE WHEN category = 'music_offtopic' THEN 1 ELSE 0 END) as categoryMusicOfftopic, ";
}
const rows = db.prepare('all', "SELECT COUNT(*) as totalSubmissions, SUM(views) as viewCount," +
"SUM((sponsorTimes.endTime - sponsorTimes.startTime) / 60 * sponsorTimes.views) as minutesSaved, " +
"SUM(votes) as userVotes, " +
additionalFields +
"IFNULL(userNames.userName, sponsorTimes.userID) as userName FROM sponsorTimes LEFT JOIN userNames ON sponsorTimes.userID=userNames.userID " +
"LEFT JOIN privateDB.shadowBannedUsers ON sponsorTimes.userID=privateDB.shadowBannedUsers.userID " +
"WHERE sponsorTimes.votes > -1 AND sponsorTimes.shadowHidden != 1 AND privateDB.shadowBannedUsers.userID IS NULL " +
"GROUP BY IFNULL(userName, sponsorTimes.userID) HAVING userVotes > 20 " +
"ORDER BY " + sortBy + " DESC LIMIT 100", []);
for (let i = 0; i < rows.length; i++) {
userNames[i] = rows[i].userName;
viewCounts[i] = rows[i].viewCount;
totalSubmissions[i] = rows[i].totalSubmissions;
minutesSaved[i] = rows[i].minutesSaved;
if (categoryStatsEnabled) {
categoryStats[i] = [
rows[i].categorySponsor,
rows[i].categorySumIntro,
rows[i].categorySumOutro,
rows[i].categorySumInteraction,
rows[i].categorySelfpromo,
rows[i].categoryMusicOfftopic,
];
}
}
resolve({
userNames,
viewCounts,
totalSubmissions,
minutesSaved,
categoryStats,
});
});
}
export async function getTopUsers(req: Request, res: Response) {
const sortType = parseInt(req.query.sortType as string);
const categoryStatsEnabled = req.query.categoryStats;
if (sortType == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//setup which sort type to use
let sortBy = '';
if (sortType == 0) {
sortBy = 'minutesSaved';
} else if (sortType == 1) {
sortBy = 'viewCount';
} else if (sortType == 2) {
sortBy = 'totalSubmissions';
} else {
//invalid request
return res.sendStatus(400);
}
const stats = await getTopUsersWithCache(sortBy, categoryStatsEnabled);
//send this result
res.send(stats);
}

View File

@@ -1,19 +1,20 @@
const db = require('../databases/databases.js').db;
const request = require('request');
const config = require('../config.js');
import {db} from '../databases/databases';
import request from 'request';
import {config} from '../config';
import {Request, Response} from 'express';
// A cache of the number of chrome web store users
let chromeUsersCache = null;
let firefoxUsersCache = null;
let chromeUsersCache = 0;
let firefoxUsersCache = 0;
// By the privacy friendly user counter
let apiUsersCache = null;
let apiUsersCache = 0;
let lastUserCountCheck = 0;
module.exports = function getTotalStats (req, res) {
export function getTotalStats(req: Request, res: Response) {
let row = db.prepare('get', "SELECT COUNT(DISTINCT userID) as userCount, COUNT(*) as totalSubmissions, " +
"SUM(views) as viewCount, SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE shadowHidden != 1 AND votes >= 0", []);
"SUM(views) as viewCount, SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE shadowHidden != 1 AND votes >= 0", []);
if (row !== undefined) {
let extensionUsers = chromeUsersCache + firefoxUsersCache;
@@ -25,7 +26,7 @@ module.exports = function getTotalStats (req, res) {
apiUsers: Math.max(apiUsersCache, extensionUsers),
viewCount: row.viewCount,
totalSubmissions: row.totalSubmissions,
minutesSaved: row.minutesSaved
minutesSaved: row.minutesSaved,
});
// Check if the cache should be updated (every ~14 hours)
@@ -49,7 +50,7 @@ function updateExtensionUsers() {
try {
firefoxUsersCache = parseInt(JSON.parse(body).average_daily_users);
request.get("https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone", function(err, chromeResponse, body) {
request.get("https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone", function (err, chromeResponse, body) {
if (body !== undefined) {
try {
chromeUsersCache = parseInt(body.match(/(?<=\<span class=\"e-f-ih\" title=\").*?(?= users\">)/)[0].replace(",", ""));
@@ -66,4 +67,4 @@ function updateExtensionUsers() {
lastUserCountCheck = 0;
}
});
}
}

View File

@@ -1,82 +0,0 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
function dbGetSubmittedSegmentSummary (userID) {
try {
let row = db.prepare("get", "SELECT SUM(((endTime - startTime) / 60) * views) as minutesSaved, count(*) as segmentCount FROM sponsorTimes WHERE userID = ? AND votes > -2 AND shadowHidden != 1", [userID]);
if (row.minutesSaved != null) {
return {
minutesSaved: row.minutesSaved,
segmentCount: row.segmentCount,
};
} else {
return {
minutesSaved: 0,
segmentCount: 0,
};
}
} catch (err) {
return false;
}
}
function dbGetUsername (userID) {
try {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
if (row !== undefined) {
return row.userName;
} else {
//no username yet, just send back the userID
return userID;
}
} catch (err) {
return false;
}
}
function dbGetViewsForUser (userID) {
try {
let row = db.prepare('get', "SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ? AND votes > -2 AND shadowHidden != 1", [userID]);
//increase the view count by one
if (row.viewCount != null) {
return row.viewCount;
} else {
return 0;
}
} catch (err) {
return false;
}
}
function dbGetWarningsForUser (userID) {
try {
let rows = db.prepare('all', "SELECT * FROM warnings WHERE userID = ?", [userID]);
return rows.length;
} catch (err) {
logger.error('Couldn\'t get warnings for user ' + userID + '. returning 0') ;
return 0;
}
}
module.exports = function getUserInfo (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.status(400).send('Parameters are not valid');
return;
}
//hash the userID
userID = getHash(userID);
const segmentsSummary = dbGetSubmittedSegmentSummary(userID);
res.send({
userID,
userName: dbGetUsername(userID),
minutesSaved: segmentsSummary.minutesSaved,
segmentCount: segmentsSummary.segmentCount,
viewCount: dbGetViewsForUser(userID),
warnings: dbGetWarningsForUser(userID)
});
}

84
src/routes/getUserInfo.ts Normal file
View File

@@ -0,0 +1,84 @@
import {db} from '../databases/databases';
import {getHash} from '../utils/getHash';
import {Request, Response} from 'express';
import {Logger} from '../utils/logger'
function dbGetSubmittedSegmentSummary(userID: string): any {
try {
let row = db.prepare("get", "SELECT SUM(((endTime - startTime) / 60) * views) as minutesSaved, count(*) as segmentCount FROM sponsorTimes WHERE userID = ? AND votes > -2 AND shadowHidden != 1", [userID]);
if (row.minutesSaved != null) {
return {
minutesSaved: row.minutesSaved,
segmentCount: row.segmentCount,
};
} else {
return {
minutesSaved: 0,
segmentCount: 0,
};
}
} catch (err) {
return false;
}
}
function dbGetUsername(userID: string) {
try {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
if (row !== undefined) {
return row.userName;
} else {
//no username yet, just send back the userID
return userID;
}
} catch (err) {
return false;
}
}
function dbGetViewsForUser(userID: string) {
try {
let row = db.prepare('get', "SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ? AND votes > -2 AND shadowHidden != 1", [userID]);
//increase the view count by one
if (row.viewCount != null) {
return row.viewCount;
} else {
return 0;
}
} catch (err) {
return false;
}
}
function dbGetWarningsForUser(userID: string): number {
try {
let rows = db.prepare('all', "SELECT * FROM warnings WHERE userID = ?", [userID]);
return rows.length;
} catch (err) {
Logger.error('Couldn\'t get warnings for user ' + userID + '. returning 0');
return 0;
}
}
export function getUserInfo(req: Request, res: Response) {
let userID = req.query.userID as string;
if (userID == undefined) {
//invalid request
res.status(400).send('Parameters are not valid');
return;
}
//hash the userID
userID = getHash(userID);
const segmentsSummary = dbGetSubmittedSegmentSummary(userID);
res.send({
userID,
userName: dbGetUsername(userID),
minutesSaved: segmentsSummary.minutesSaved,
segmentCount: segmentsSummary.segmentCount,
viewCount: dbGetViewsForUser(userID),
warnings: dbGetWarningsForUser(userID),
});
}

View File

@@ -1,10 +1,10 @@
var db = require('../databases/databases.js').db;
import {db} from '../databases/databases';
import {getHash} from '../utils/getHash';
import {Logger} from '../utils/logger';
import {Request, Response} from 'express';
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = function getUsername (req, res) {
let userID = req.query.userID;
export function getUsername(req: Request, res: Response) {
let userID = req.query.userID as string;
if (userID == undefined) {
//invalid request
@@ -20,18 +20,18 @@ module.exports = function getUsername (req, res) {
if (row !== undefined) {
res.send({
userName: row.userName
userName: row.userName,
});
} else {
//no username yet, just send back the userID
res.send({
userName: userID
userName: userID,
});
}
} catch (err) {
logger.error(err);
Logger.error(err);
res.sendStatus(500);
return;
}
}
}

View File

@@ -1,33 +0,0 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
var logger = require('../utils/logger.js');
module.exports = function getViewsForUser(req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare('get', "SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ?", [userID]);
//increase the view count by one
if (row.viewCount != null) {
res.send({
viewCount: row.viewCount
});
} else {
res.sendStatus(404);
}
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -0,0 +1,35 @@
import {db} from '../databases/databases';
import {Request, Response} from 'express';
import {getHash} from '../utils/getHash';
import {Logger} from '../utils/logger';
export function getViewsForUser(req: Request, res: Response) {
let userID = req.query.userID as string;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let row = db.prepare('get', "SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ?", [userID]);
//increase the view count by one
if (row.viewCount != null) {
res.send({
viewCount: row.viewCount,
});
} else {
res.sendStatus(404);
}
} catch (err) {
Logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -1,10 +1,8 @@
var getSkipSegments = require("./getSkipSegments.js")
import {handleGetSegments} from './getSkipSegments';
import {Request, Response} from 'express';
module.exports = function (req, res) {
let videoID = req.query.videoID;
let segments = getSkipSegments.handleGetSegments(req, res);
export function oldGetVideoSponsorTimes(req: Request, res: Response) {
let segments = handleGetSegments(req, res);
if (segments) {
// Convert to old outputs
@@ -18,9 +16,9 @@ module.exports = function (req, res) {
res.send({
sponsorTimes,
UUIDs
})
UUIDs,
});
}
// Error has already been handled in the other method
}
}

View File

@@ -1,7 +0,0 @@
var postSkipSegments = require('./postSkipSegments.js');
module.exports = async function submitSponsorTimes(req, res) {
req.query.category = "sponsor";
return postSkipSegments(req, res);
}

View File

@@ -0,0 +1,8 @@
import {postSkipSegments} from './postSkipSegments';
import {Request, Response} from 'express';
export async function oldSubmitSponsorTimes(req: Request, res: Response) {
req.query.category = "sponsor";
return postSkipSegments(req, res);
}

View File

@@ -1,23 +1,24 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
import {Logger} from '../utils/logger';
import {getHash} from '../utils/getHash';
import {isUserVIP} from '../utils/isUserVIP';
import {db} from '../databases/databases';
import {Request, Response} from 'express';
module.exports = (req, res) => {
export function postNoSegments(req: Request, res: Response) {
// Collect user input data
let videoID = req.body.videoID;
let userID = req.body.userID;
let categories = req.body.categories;
// Check input data is valid
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
) {
res.status(400).json({
message: 'Bad Format'
message: 'Bad Format',
});
return;
}
@@ -28,7 +29,7 @@ module.exports = (req, res) => {
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to mark videos.'
message: 'Must be a VIP to mark videos.',
});
return;
}
@@ -38,7 +39,7 @@ module.exports = (req, res) => {
if (!noSegmentList || noSegmentList.length === 0) {
noSegmentList = [];
} else {
noSegmentList = noSegmentList.map((obj) => {
noSegmentList = noSegmentList.map((obj: any) => {
return obj.category;
});
}
@@ -60,15 +61,15 @@ module.exports = (req, res) => {
try {
db.prepare('run', "INSERT INTO noSegments (videoID, userID, category) VALUES(?, ?, ?)", [videoID, userID, category]);
} catch (err) {
logger.error("Error submitting 'noSegment' marker for category '" + category + "' for video '" + videoID + "'");
logger.error(err);
Logger.error("Error submitting 'noSegment' marker for category '" + category + "' for video '" + videoID + "'");
Logger.error(err);
res.status(500).json({
message: "Internal Server Error: Could not write marker to the database."
message: "Internal Server Error: Could not write marker to the database.",
});
}
});
res.status(200).json({
submitted: categoriesToMark
});
};
submitted: categoriesToMark,
});
}

View File

@@ -1,101 +0,0 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
const ACTION_NONE = Symbol('none');
const ACTION_UPDATE = Symbol('update');
const ACTION_REMOVE = Symbol('remove');
function shiftSegment(segment, shift) {
if (segment.startTime >= segment.endTime) return {action: ACTION_NONE, segment};
if (shift.startTime >= shift.endTime) return {action: ACTION_NONE, segment};
const duration = shift.endTime - shift.startTime;
if (shift.endTime < segment.startTime) {
// Scenario #1 cut before segment
segment.startTime -= duration;
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (shift.startTime > segment.endTime) {
// Scenario #2 cut after segment
return {action: ACTION_NONE, segment};
}
if (segment.startTime < shift.startTime && segment.endTime > shift.endTime) {
// Scenario #3 cut inside segment
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime >= shift.startTime && segment.endTime > shift.endTime) {
// Scenario #4 cut overlap startTime
segment.startTime = shift.startTime;
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime < shift.startTime && segment.endTime <= shift.endTime) {
// Scenario #5 cut overlap endTime
segment.endTime = shift.startTime;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime >= shift.startTime && segment.endTime <= shift.endTime) {
// Scenario #6 cut overlap startTime and endTime
return {action: ACTION_REMOVE, segment};
}
return {action: ACTION_NONE, segment};
}
module.exports = (req, res) => {
// Collect user input data
const videoID = req.body.videoID;
const startTime = req.body.startTime;
const endTime = req.body.endTime;
let userID = req.body.userID;
// Check input data is valid
if (!videoID
|| !userID
|| !startTime
|| !endTime
) {
res.status(400).json({
message: 'Bad Format'
});
return;
}
// Check if user is VIP
userID = getHash(userID);
const userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to perform this action.'
});
return;
}
try {
const segments = db.prepare('all', 'SELECT startTime, endTime, UUID FROM sponsorTimes WHERE videoID = ?', [videoID]);
const shift = {
startTime,
endTime,
};
segments.forEach(segment => {
const result = shiftSegment(segment, shift);
switch (result.action) {
case ACTION_UPDATE:
db.prepare('run', 'UPDATE sponsorTimes SET startTime = ?, endTime = ? WHERE UUID = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
break;
case ACTION_REMOVE:
db.prepare('run', 'UPDATE sponsorTimes SET startTime = ?, endTime = ?, votes = -2 WHERE UUID = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
break;
}
});
}
catch(err) {
logger.error(err);
res.sendStatus(500);
}
res.sendStatus(200);
};

View File

@@ -0,0 +1,101 @@
import {Request, Response} from 'express';
import {Logger} from '../utils/logger';
import {isUserVIP} from '../utils/isUserVIP';
import {getHash} from '../utils/getHash';
import {db} from '../databases/databases';
const ACTION_NONE = Symbol('none');
const ACTION_UPDATE = Symbol('update');
const ACTION_REMOVE = Symbol('remove');
function shiftSegment(segment: any, shift: { startTime: any; endTime: any }) {
if (segment.startTime >= segment.endTime) return {action: ACTION_NONE, segment};
if (shift.startTime >= shift.endTime) return {action: ACTION_NONE, segment};
const duration = shift.endTime - shift.startTime;
if (shift.endTime < segment.startTime) {
// Scenario #1 cut before segment
segment.startTime -= duration;
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (shift.startTime > segment.endTime) {
// Scenario #2 cut after segment
return {action: ACTION_NONE, segment};
}
if (segment.startTime < shift.startTime && segment.endTime > shift.endTime) {
// Scenario #3 cut inside segment
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime >= shift.startTime && segment.endTime > shift.endTime) {
// Scenario #4 cut overlap startTime
segment.startTime = shift.startTime;
segment.endTime -= duration;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime < shift.startTime && segment.endTime <= shift.endTime) {
// Scenario #5 cut overlap endTime
segment.endTime = shift.startTime;
return {action: ACTION_UPDATE, segment};
}
if (segment.startTime >= shift.startTime && segment.endTime <= shift.endTime) {
// Scenario #6 cut overlap startTime and endTime
return {action: ACTION_REMOVE, segment};
}
return {action: ACTION_NONE, segment};
}
export function postSegmentShift(req: Request, res: Response): Response {
// Collect user input data
const videoID = req.body.videoID;
const startTime = req.body.startTime;
const endTime = req.body.endTime;
let userID = req.body.userID;
// Check input data is valid
if (!videoID
|| !userID
|| !startTime
|| !endTime
) {
res.status(400).json({
message: 'Bad Format',
});
return;
}
// Check if user is VIP
userID = getHash(userID);
const userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to perform this action.',
});
return;
}
try {
const segments = db.prepare('all', 'SELECT startTime, endTime, UUID FROM sponsorTimes WHERE videoID = ?', [videoID]);
const shift = {
startTime,
endTime,
};
segments.forEach((segment: any) => {
const result = shiftSegment(segment, shift);
switch (result.action) {
case ACTION_UPDATE:
db.prepare('run', 'UPDATE sponsorTimes SET startTime = ?, endTime = ? WHERE UUID = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
break;
case ACTION_REMOVE:
db.prepare('run', 'UPDATE sponsorTimes SET startTime = ?, endTime = ?, votes = -2 WHERE UUID = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
break;
}
});
} catch (err) {
Logger.error(err);
res.sendStatus(500);
}
res.sendStatus(200);
}

View File

@@ -1,25 +1,23 @@
const config = require('../config.js');
import {config} from '../config';
import {Logger} from '../utils/logger';
import {db, privateDB} from '../databases/databases';
import {YouTubeAPI} from '../utils/youtubeApi';
import {getSubmissionUUID} from '../utils/getSubmissionUUID';
import request from 'request';
import isoDurations from 'iso8601-duration';
import fetch from 'node-fetch';
import {getHash} from '../utils/getHash';
import {getIP} from '../utils/getIP';
import {getFormattedTime} from '../utils/getFormattedTime';
import {isUserTrustworthy} from '../utils/isUserTrustworthy';
import {dispatchEvent} from '../utils/webhookUtils';
import {Request, Response} from 'express';
const databases = require('../databases/databases.js');
const db = databases.db;
const privateDB = databases.privateDB;
const YouTubeAPI = require('../utils/youtubeAPI.js');
const logger = require('../utils/logger.js');
const getSubmissionUUID = require('../utils/getSubmissionUUID.js');
const request = require('request');
const isoDurations = require('iso8601-duration');
const fetch = require('node-fetch');
const getHash = require('../utils/getHash.js');
const getIP = require('../utils/getIP.js');
const getFormattedTime = require('../utils/getFormattedTime.js');
const isUserTrustworthy = require('../utils/isUserTrustworthy.js')
const { dispatchEvent } = require('../utils/webhookUtils.js');
function sendWebhookNotification(userID, videoID, UUID, submissionCount, youtubeData, {submissionStart, submissionEnd}, segmentInfo) {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
let userName = row !== undefined ? row.userName : null;
let video = youtubeData.items[0];
function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: any, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
const row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
const userName = row !== undefined ? row.userName : null;
const video = youtubeData.items[0];
let scopeName = "submissions.other";
if (submissionCount <= 1) {
@@ -31,7 +29,7 @@ function sendWebhookNotification(userID, videoID, UUID, submissionCount, youtube
"id": videoID,
"title": video.snippet.title,
"thumbnail": video.snippet.thumbnails.maxres ? video.snippet.thumbnails.maxres : null,
"url": "https://www.youtube.com/watch?v=" + videoID
"url": "https://www.youtube.com/watch?v=" + videoID,
},
"submission": {
"UUID": UUID,
@@ -40,25 +38,28 @@ function sendWebhookNotification(userID, videoID, UUID, submissionCount, youtube
"endTime": submissionEnd,
"user": {
"UUID": userID,
"username": userName
}
}
"username": userName,
},
},
});
}
function sendWebhooks(userID, videoID, UUID, segmentInfo) {
function sendWebhooks(userID: string, videoID: string, UUID: string, segmentInfo: any) {
if (config.youtubeAPIKey !== null) {
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [userID]);
const userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [userID]);
YouTubeAPI.listVideos(videoID, (err, data) => {
YouTubeAPI.listVideos(videoID, (err: any, data: any) => {
if (err || data.items.length === 0) {
err && logger.error(err);
err && Logger.error(err);
return;
}
let startTime = parseFloat(segmentInfo.segment[0]);
let endTime = parseFloat(segmentInfo.segment[1]);
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, data, {submissionStart: startTime, submissionEnd: endTime}, segmentInfo);
const startTime = parseFloat(segmentInfo.segment[0]);
const endTime = parseFloat(segmentInfo.segment[1]);
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, data, {
submissionStart: startTime,
submissionEnd: endTime,
}, segmentInfo);
// If it is a first time submission
// Then send a notification to discord
@@ -67,45 +68,45 @@ function sendWebhooks(userID, videoID, UUID, segmentInfo) {
json: {
"embeds": [{
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (startTime.toFixed(0) - 2),
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (parseInt(startTime.toFixed(0)) - 2),
"description": "Submission ID: " + UUID +
"\n\nTimestamp: " +
getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n\nCategory: " + segmentInfo.category,
"\n\nTimestamp: " +
getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n\nCategory: " + segmentInfo.category,
"color": 10813440,
"author": {
"name": userID
"name": userID,
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
},
}],
},
}, (err, res) => {
if (err) {
logger.error("Failed to send first time submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending first time submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
if (err) {
Logger.error("Failed to send first time submission Discord hook.");
Logger.error(JSON.stringify(err));
Logger.error("\n");
} else if (res && res.statusCode >= 400) {
Logger.error("Error sending first time submission Discord hook");
Logger.error(JSON.stringify(res));
Logger.error("\n");
}
});
});
}
}
function sendWebhooksNB(userID, videoID, UUID, startTime, endTime, category, probability, ytData) {
let submissionInfoRow = db.prepare('get', "SELECT " +
function sendWebhooksNB(userID: string, videoID: string, UUID: string, startTime: number, endTime: number, category: string, probability: number, ytData: any) {
const submissionInfoRow = db.prepare('get', "SELECT " +
"(select count(1) from sponsorTimes where userID = ?) count, " +
"(select count(1) from sponsorTimes where userID = ? and votes <= -2) disregarded, " +
"coalesce((select userName FROM userNames WHERE userID = ?), ?) userName",
[userID, userID, userID, userID]);
let submittedBy = "";
let submittedBy: string;
// If a userName was created then show both
if (submissionInfoRow.userName !== userID){
if (submissionInfoRow.userName !== userID) {
submittedBy = submissionInfoRow.userName + "\n " + userID;
} else {
submittedBy = userID;
@@ -117,30 +118,30 @@ function sendWebhooksNB(userID, videoID, UUID, startTime, endTime, category, pro
json: {
"embeds": [{
"title": ytData.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (startTime.toFixed(0) - 2),
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (parseFloat(startTime.toFixed(0)) - 2),
"description": "**Submission ID:** " + UUID +
"\n**Timestamp:** " + getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n**Predicted Probability:** " + probability +
"\n**Category:** " + category +
"\n**Submitted by:** "+ submittedBy +
"\n**Total User Submissions:** "+submissionInfoRow.count +
"\n**Ignored User Submissions:** "+submissionInfoRow.disregarded,
"\n**Timestamp:** " + getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n**Predicted Probability:** " + probability +
"\n**Category:** " + category +
"\n**Submitted by:** " + submittedBy +
"\n**Total User Submissions:** " + submissionInfoRow.count +
"\n**Ignored User Submissions:** " + submissionInfoRow.disregarded,
"color": 10813440,
"thumbnail": {
"url": ytData.items[0].snippet.thumbnails.maxres ? ytData.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
},
}],
},
}, (err, res) => {
if (err) {
logger.error("Failed to send NeuralBlock Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending NeuralBlock Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
if (err) {
Logger.error("Failed to send NeuralBlock Discord hook.");
Logger.error(JSON.stringify(err));
Logger.error("\n");
} else if (res && res.statusCode >= 400) {
Logger.error("Error sending NeuralBlock Discord hook");
Logger.error(JSON.stringify(res));
Logger.error("\n");
}
});
}
@@ -150,11 +151,11 @@ function sendWebhooksNB(userID, videoID, UUID, startTime, endTime, category, pro
// Looks like this was broken for no defined youtube key - fixed but IMO we shouldn't return
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(submission) {
async function autoModerateSubmission(submission: { videoID: any; userID: any; segments: any }) {
// Get the video information from the youtube API
if (config.youtubeAPIKey !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.listVideos(submission.videoID, (err, data) => resolve({err, data}));
const {err, data} = await new Promise((resolve) => {
YouTubeAPI.listVideos(submission.videoID, (err: any, data: any) => resolve({err, data}));
});
if (err) {
@@ -164,54 +165,54 @@ async function autoModerateSubmission(submission) {
if (data.pageInfo.totalResults === 0) {
return "No video exists with id " + submission.videoID;
} else {
let segments = submission.segments;
const segments = submission.segments;
let nbString = "";
for (let i = 0; i < segments.length; i++) {
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
const startTime = parseFloat(segments[i].segment[0]);
const endTime = parseFloat(segments[i].segment[1]);
let duration = data.items[0].contentDetails.duration;
duration = isoDurations.toSeconds(isoDurations.parse(duration));
if (duration == 0) {
// Allow submission if the duration is 0 (bug in youtube api)
return false;
} else if ((endTime - startTime) > (duration/100)*80) {
} else if ((endTime - startTime) > (duration / 100) * 80) {
// Reject submission if over 80% of the video
return "One of your submitted segments is over 80% of the video.";
} else {
if (segments[i].category === "sponsor") {
//Prepare timestamps to send to NB all at once
nbString = nbString + segments[i].segment[0] + "," + segments[i].segment[1] + ";";
//Prepare timestamps to send to NB all at once
nbString = nbString + segments[i].segment[0] + "," + segments[i].segment[1] + ";";
}
}
}
// Check NeuralBlock
let neuralBlockURL = config.neuralBlockURL;
const neuralBlockURL = config.neuralBlockURL;
if (!neuralBlockURL) return false;
let response = await fetch(neuralBlockURL + "/api/checkSponsorSegments?vid=" + submission.videoID +
"&segments=" + nbString.substring(0,nbString.length-1));
const response = await fetch(neuralBlockURL + "/api/checkSponsorSegments?vid=" + submission.videoID +
"&segments=" + nbString.substring(0, nbString.length - 1));
if (!response.ok) return false;
let nbPredictions = await response.json();
nbDecision = false;
const nbPredictions = await response.json();
let nbDecision = false;
let predictionIdx = 0; //Keep track because only sponsor categories were submitted
for (let i = 0; i < segments.length; i++){
if (segments[i].category === "sponsor"){
if (nbPredictions.probabilities[predictionIdx] < 0.70){
nbDecision = true; // At least one bad entry
startTime = parseFloat(segments[i].segment[0]);
endTime = parseFloat(segments[i].segment[1]);
for (let i = 0; i < segments.length; i++) {
if (segments[i].category === "sponsor") {
if (nbPredictions.probabilities[predictionIdx] < 0.70) {
nbDecision = true; // At least one bad entry
const startTime = parseFloat(segments[i].segment[0]);
const endTime = parseFloat(segments[i].segment[1]);
const UUID = getSubmissionUUID(submission.videoID, segments[i].category, submission.userID, startTime, endTime);
// 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);
const UUID = getSubmissionUUID(submission.videoID, segments[i].category, submission.userID, startTime, endTime);
// 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);
}
predictionIdx++;
}
}
if (nbDecision){
if (nbDecision) {
return "Rejected based on NeuralBlock predictions.";
} else {
return false;
@@ -219,7 +220,7 @@ async function autoModerateSubmission(submission) {
}
}
} else {
logger.debug("Skipped YouTube API");
Logger.debug("Skipped YouTube API");
// Can't moderate the submission without calling the youtube API
// so allow by default.
@@ -227,33 +228,33 @@ async function autoModerateSubmission(submission) {
}
}
function proxySubmission(req) {
request.post(config.proxySubmission + '/api/skipSegments?userID='+req.query.userID+'&videoID='+req.query.videoID, {json: req.body}, (err, result) => {
function proxySubmission(req: Request) {
request.post(config.proxySubmission + '/api/skipSegments?userID=' + req.query.userID + '&videoID=' + req.query.videoID, {json: req.body}, (err, result) => {
if (config.mode === 'development') {
if (!err) {
logger.debug('Proxy Submission: ' + result.statusCode + ' ('+result.body+')');
Logger.debug('Proxy Submission: ' + result.statusCode + ' (' + result.body + ')');
} else {
logger.error("Proxy Submission: Failed to make call");
Logger.error("Proxy Submission: Failed to make call");
}
}
});
}
module.exports = async function postSkipSegments(req, res) {
export async function postSkipSegments(req: Request, res: Response) {
if (config.proxySubmission) {
proxySubmission(req);
}
let videoID = req.query.videoID || req.body.videoID;
const videoID = req.query.videoID || req.body.videoID;
let userID = req.query.userID || req.body.userID;
let segments = req.body.segments;
let segments = req.body.segments;
if (segments === undefined) {
// Use query instead
segments = [{
segment: [req.query.startTime, req.query.endTime],
category: req.query.category
category: req.query.category,
}];
}
@@ -279,24 +280,26 @@ module.exports = async function postSkipSegments(req, res) {
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
const hashedIP = getHash(getIP(req) + config.globalSalt);
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
let warningsCount = db.prepare('get', "SELECT count(1) as count FROM warnings WHERE userID = ? AND issueTime > ?",
[userID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))]
const warningsCount = db.prepare('get', "SELECT count(1) as count FROM warnings WHERE userID = ? AND issueTime > ?",
[userID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
).count;
if (warningsCount >= config.maxNumberOfActiveWarnings) {
return res.status(403).send('Submission blocked. Too many active warnings!');
return res.status(403).send('Submission blocked. Too many active warnings!');
}
let noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]).map((list) => { return list.category });
//check if this user is on the vip list
let isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
const noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]).map((list: any) => {
return list.category;
});
let decreaseVotes = 0;
//check if this user is on the vip list
const isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
const decreaseVotes = 0;
// Check if all submissions are correct
for (let i = 0; i < segments.length; i++) {
@@ -305,45 +308,45 @@ module.exports = async function postSkipSegments(req, res) {
res.status(400).send("One of your segments are invalid");
return;
}
if (!config.categoryList.includes(segments[i].category)) {
res.status("400").send("Category doesn't exist.");
return;
res.status(400).send("Category doesn't exist.");
return;
}
// Reject segemnt if it's in the no segments list
if (!isVIP && noSegmentList.indexOf(segments[i].category) !== -1) {
// TODO: Do something about the fradulent submission
logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'");
Logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'");
res.status(403).send(
"Request rejected by auto moderator: 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."
"Request rejected by auto moderator: 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.",
);
return;
}
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
if (isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime >= endTime) {
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime >= endTime) {
//invalid request
res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)");
return;
}
if (segments[i].category === "sponsor" && Math.abs(startTime - endTime) < 1) {
if (!isVIP && segments[i].category === "sponsor" && Math.abs(startTime - endTime) < 1) {
// Too short
res.status(400).send("Sponsors must be longer than 1 second long");
return;
}
//check if this info has already been submitted before
let duplicateCheck2Row = db.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE startTime = ? " +
const duplicateCheck2Row = db.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE startTime = ? " +
"and endTime = ? and category = ? and videoID = ?", [startTime, endTime, segments[i].category, videoID]);
if (duplicateCheck2Row.count > 0) {
res.sendStatus(409);
@@ -353,8 +356,8 @@ module.exports = async function postSkipSegments(req, res) {
// Auto moderator check
if (!isVIP) {
let autoModerateResult = await autoModerateSubmission({userID, videoID, segments});//startTime, endTime, category: segments[i].category});
if (autoModerateResult == "Rejected based on NeuralBlock predictions."){
const autoModerateResult = await autoModerateSubmission({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
@@ -367,18 +370,18 @@ module.exports = async function postSkipSegments(req, res) {
}
}
// Will be filled when submitting
let UUIDs = [];
const UUIDs = [];
try {
//get current time
let timeSubmitted = Date.now();
const timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
const yesterday = timeSubmitted - 86400000;
// Disable IP ratelimiting for now
if (false) {
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?", [hashedIP, videoID, yesterday]);
const rateLimitCheckRow = 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
@@ -391,7 +394,7 @@ module.exports = async function postSkipSegments(req, res) {
// Disable max submissions for now
if (false) {
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?", [userID, videoID]);
const duplicateCheckRow = 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
@@ -402,7 +405,7 @@ module.exports = async function postSkipSegments(req, res) {
}
//check to see if this user is shadowbanned
let shadowBanRow = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
const shadowBanRow = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
let shadowBanned = shadowBanRow.userCount;
@@ -418,20 +421,20 @@ module.exports = async function postSkipSegments(req, res) {
}
if (config.youtubeAPIKey !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.listVideos(videoID, (err, data) => resolve({err, data}));
let {err, data} = await new Promise((resolve) => {
YouTubeAPI.listVideos(videoID, (err: any, data: any) => resolve({err, data}));
});
if (err) {
logger.error("Error while submitting when connecting to YouTube API: " + err);
Logger.error("Error while submitting when connecting to YouTube API: " + err);
} else {
//get all segments for this video and user
let allSubmittedByUser = db.prepare('all', "SELECT startTime, endTime FROM sponsorTimes WHERE userID = ? and videoID = ? and votes > -1", [userID, videoID]);
let allSegmentTimes = [];
const allSubmittedByUser = db.prepare('all', "SELECT startTime, endTime FROM sponsorTimes WHERE userID = ? and videoID = ? and votes > -1", [userID, videoID]);
const allSegmentTimes = [];
if (allSubmittedByUser !== undefined) {
//add segments the user has previously submitted
for (const segmentInfo of allSubmittedByUser) {
allSegmentTimes.push([parseFloat(segmentInfo.startTime), parseFloat(segmentInfo.endTime)])
allSegmentTimes.push([parseFloat(segmentInfo.startTime), parseFloat(segmentInfo.endTime)]);
}
}
@@ -443,7 +446,9 @@ module.exports = async function postSkipSegments(req, res) {
}
//merge all the times into non-overlapping arrays
const allSegmentsSorted = mergeTimeSegments(allSegmentTimes.sort(function(a, b) { return a[0]-b[0] || a[1]-b[1] }));
const allSegmentsSorted = mergeTimeSegments(allSegmentTimes.sort(function (a, b) {
return a[0] - b[0] || a[1] - b[1];
}));
let videoDuration = data.items[0].contentDetails.duration;
videoDuration = isoDurations.toSeconds(isoDurations.parse(videoDuration));
@@ -451,7 +456,7 @@ module.exports = async function postSkipSegments(req, res) {
let allSegmentDuration = 0;
//sum all segment times together
allSegmentsSorted.forEach(segmentInfo => allSegmentDuration += segmentInfo[1] - segmentInfo[0]);
if (allSegmentDuration > (videoDuration/100)*80) {
if (allSegmentDuration > (videoDuration / 100) * 80) {
// Reject submission if all segments combine are over 80% of the video
res.status(400).send("Total length of your submitted segments are over 80% of the video.");
return;
@@ -467,19 +472,19 @@ module.exports = async function postSkipSegments(req, res) {
const UUID = getSubmissionUUID(videoID, segmentInfo.category, userID, segmentInfo.segment[0], segmentInfo.segment[1]);
try {
db.prepare('run', "INSERT INTO sponsorTimes " +
db.prepare('run', "INSERT INTO sponsorTimes " +
"(videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID)" +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned, getHash(videoID, 1)
]
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned, getHash(videoID, 1),
],
);
//add to private db as well
privateDB.prepare('run', "INSERT INTO sponsorTimes VALUES(?, ?, ?)", [videoID, hashedIP, timeSubmitted]);
} catch (err) {
//a DB change probably occurred
res.sendStatus(502);
logger.error("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
Logger.error("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
segmentInfo.segment[1] + ", " + userID + ", " + segmentInfo.category + ". " + err);
return;
@@ -488,7 +493,7 @@ module.exports = async function postSkipSegments(req, res) {
UUIDs.push(UUID);
}
} catch (err) {
logger.error(err);
Logger.error(err);
res.sendStatus(500);
@@ -516,8 +521,9 @@ module.exports = async function postSkipSegments(req, res) {
// [50, 80],
// [100, 150]
// ]
function mergeTimeSegments(ranges) {
var result = [], last;
function mergeTimeSegments(ranges: number[][]) {
const result: number[][] = [];
let last: number[];
ranges.forEach(function (r) {
if (!last || r[0] > last[1])
@@ -527,4 +533,4 @@ function mergeTimeSegments(ranges) {
});
return result;
}
}

View File

@@ -1,9 +1,10 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
import {Request, Response} from 'express';
import {Logger} from '../utils/logger';
import {db} from '../databases/databases';
import {isUserVIP} from '../utils/isUserVIP';
import {getHash} from '../utils/getHash';
module.exports = (req, res) => {
export function postWarning(req: Request, res: Response) {
// Collect user input data
let issuerUserID = getHash(req.body.issuerUserID);
let userID = getHash(req.body.userID);
@@ -11,14 +12,14 @@ module.exports = (req, res) => {
// Ensure user is a VIP
if (!isUserVIP(issuerUserID)) {
logger.debug("Permission violation: User " + issuerUserID + " attempted to warn user " + userID + "."); // maybe warn?
Logger.debug("Permission violation: User " + issuerUserID + " attempted to warn user " + userID + "."); // maybe warn?
res.status(403).json({"message": "Not a VIP"});
return;
}
db.prepare('run', 'INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES (?, ?, ?)', [userID, issueTime, issuerUserID]);
res.status(200).json({
message: "Warning issued to user '" + userID + "'."
message: "Warning issued to user '" + userID + "'.",
});
};
}

View File

@@ -1,60 +0,0 @@
var config = require('../config.js');
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = function setUsername(req, res) {
let userID = req.query.userID;
let userName = req.query.username;
let adminUserIDInput = req.query.adminUserID;
if (userID == undefined || userName == undefined || userID === "undefined" || userName.length > 64) {
//invalid request
res.sendStatus(400);
return;
}
if (userName.includes("discord")) {
// Don't allow
res.sendStatus(200);
return;
}
if (adminUserIDInput != undefined) {
//this is the admin controlling the other users account, don't hash the controling account's ID
adminUserIDInput = getHash(adminUserIDInput);
if (adminUserIDInput != config.adminUserID) {
//they aren't the admin
res.sendStatus(403);
return;
}
} else {
//hash the userID
userID = getHash(userID);
}
try {
//check if username is already set
let row = db.prepare('get', "SELECT count(*) as count FROM userNames WHERE userID = ?", [userID]);
if (row.count > 0) {
//already exists, update this row
db.prepare('run', "UPDATE userNames SET userName = ? WHERE userID = ?", [userName, userID]);
} else {
//add to the db
db.prepare('run', "INSERT INTO userNames VALUES(?, ?)", [userID, userName]);
}
res.sendStatus(200);
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

58
src/routes/setUsername.ts Normal file
View File

@@ -0,0 +1,58 @@
import {config} from '../config';
import {Logger} from '../utils/logger';
import {db} from '../databases/databases';
import {getHash} from '../utils/getHash';
import {Request, Response} from 'express';
export function setUsername(req: Request, res: Response) {
let userID = req.query.userID as string;
let userName = req.query.username as string;
let adminUserIDInput = req.query.adminUserID as string;
if (userID == undefined || userName == undefined || userID === "undefined" || userName.length > 64) {
//invalid request
res.sendStatus(400);
return;
}
if (userName.includes("discord")) {
// Don't allow
res.sendStatus(200);
return;
}
if (adminUserIDInput != undefined) {
//this is the admin controlling the other users account, don't hash the controling account's ID
adminUserIDInput = getHash(adminUserIDInput);
if (adminUserIDInput != config.adminUserID) {
//they aren't the admin
res.sendStatus(403);
return;
}
} else {
//hash the userID
userID = getHash(userID);
}
try {
//check if username is already set
let row = db.prepare('get', "SELECT count(*) as count FROM userNames WHERE userID = ?", [userID]);
if (row.count > 0) {
//already exists, update this row
db.prepare('run', "UPDATE userNames SET userName = ? WHERE userID = ?", [userName, userID]);
} else {
//add to the db
db.prepare('run', "INSERT INTO userNames VALUES(?, ?)", [userID, userName]);
}
res.sendStatus(200);
} catch (err) {
Logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -1,100 +0,0 @@
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
module.exports = async function shadowBanUser(req, res) {
let userID = req.query.userID;
let hashedIP = req.query.hashedIP;
let adminUserIDInput = req.query.adminUserID;
let enabled = req.query.enabled;
if (enabled === undefined){
enabled = true;
} else {
enabled = enabled === "true";
}
//if enabled is false and the old submissions should be made visible again
let unHideOldSubmissions = req.query.unHideOldSubmissions !== "false";
if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined)) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
adminUserIDInput = getHash(adminUserIDInput);
let isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [adminUserIDInput]).userCount > 0;
if (!isVIP) {
//not authorized
res.sendStatus(403);
return;
}
if (userID) {
//check to see if this user is already shadowbanned
let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
if (enabled && row.userCount == 0) {
//add them to the shadow ban list
//add it to the table
privateDB.prepare('run', "INSERT INTO shadowBannedUsers VALUES(?)", [userID]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE userID = ?", [userID]);
}
} else if (!enabled && row.userCount > 0) {
//remove them from the shadow ban list
privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
//find all previous submissions and unhide them
if (unHideOldSubmissions) {
let segmentsToIgnore = db.prepare('all', "SELECT uuid FROM sponsorTimes st JOIN noSegments ns on st.videoID = ns.videoID AND st.category = ns.category WHERE st.userID = ?", [userID]).map((item) => item.UUID);
let allSegments = db.prepare('all', "SELECT uuid FROM sponsorTimes st WHERE st.userID = ?", [userID]).map((item) => item.UUID);
allSegments.filter((item) => {
return segmentsToIgnore.indexOf(item) === -1;
}).forEach((uuid) => {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE uuid = ?", [uuid]);
});
}
}
} else if (hashedIP) {
//check to see if this user is already shadowbanned
// let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedIPs WHERE hashedIP = ?", [hashedIP]);
// if (enabled && row.userCount == 0) {
if (enabled) {
//add them to the shadow ban list
//add it to the table
// privateDB.prepare('run', "INSERT INTO shadowBannedIPs VALUES(?)", [hashedIP]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE timeSubmitted IN " +
"(SELECT privateDB.timeSubmitted FROM sponsorTimes LEFT JOIN privateDB.sponsorTimes as privateDB ON sponsorTimes.timeSubmitted=privateDB.timeSubmitted " +
"WHERE privateDB.hashedIP = ?)", [hashedIP]);
}
} else if (!enabled && row.userCount > 0) {
// //remove them from the shadow ban list
// privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
// //find all previous submissions and unhide them
// if (unHideOldSubmissions) {
// db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE userID = ?", [userID]);
// }
}
}
res.sendStatus(200);
}

View File

@@ -0,0 +1,99 @@
import {db, privateDB} from '../databases/databases';
import {getHash} from '../utils/getHash';
import {Request, Response} from 'express';
export async function shadowBanUser(req: Request, res: Response) {
const userID = req.query.userID as string;
const hashedIP = req.query.hashedIP as string;
let adminUserIDInput = req.query.adminUserID as string;
const enabled = req.query.enabled === undefined
? false
: req.query.enabled === 'true';
//if enabled is false and the old submissions should be made visible again
const unHideOldSubmissions = req.query.unHideOldSubmissions !== "false";
if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined)) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
adminUserIDInput = getHash(adminUserIDInput);
const isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [adminUserIDInput]).userCount > 0;
if (!isVIP) {
//not authorized
res.sendStatus(403);
return;
}
if (userID) {
//check to see if this user is already shadowbanned
const row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
if (enabled && row.userCount == 0) {
//add them to the shadow ban list
//add it to the table
privateDB.prepare('run', "INSERT INTO shadowBannedUsers VALUES(?)", [userID]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE userID = ?"
+ " AND NOT EXISTS ( SELECT videoID, category FROM noSegments WHERE"
+ " sponsorTimes.videoID = noSegments.videoID AND sponsorTimes.category = noSegments.category)", [userID]);
}
} else if (!enabled && row.userCount > 0) {
//remove them from the shadow ban list
privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
//find all previous submissions and unhide them
if (unHideOldSubmissions) {
let segmentsToIgnore = db.prepare('all', "SELECT UUID FROM sponsorTimes st "
+ "JOIN noSegments ns on st.videoID = ns.videoID AND st.category = ns.category WHERE st.userID = ?"
, [userID]).map((item: {UUID: string}) => item.UUID);
let allSegments = db.prepare('all', "SELECT UUID FROM sponsorTimes st WHERE st.userID = ?", [userID])
.map((item: {UUID: string}) => item.UUID);
allSegments.filter((item: {uuid: string}) => {
return segmentsToIgnore.indexOf(item) === -1;
}).forEach((UUID: string) => {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE UUID = ?", [UUID]);
});
}
}
}
else if (hashedIP) {
//check to see if this user is already shadowbanned
// let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedIPs WHERE hashedIP = ?", [hashedIP]);
// if (enabled && row.userCount == 0) {
if (enabled) {
//add them to the shadow ban list
//add it to the table
// privateDB.prepare('run', "INSERT INTO shadowBannedIPs VALUES(?)", [hashedIP]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE timeSubmitted IN " +
"(SELECT privateDB.timeSubmitted FROM sponsorTimes LEFT JOIN privateDB.sponsorTimes as privateDB ON sponsorTimes.timeSubmitted=privateDB.timeSubmitted " +
"WHERE privateDB.hashedIP = ?)", [hashedIP]);
}
} /*else if (!enabled && row.userCount > 0) {
// //remove them from the shadow ban list
// privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
// //find all previous submissions and unhide them
// if (unHideOldSubmissions) {
// db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE userID = ?", [userID]);
// }
}*/
}
res.sendStatus(200);
}

View File

@@ -1,16 +0,0 @@
var db = require('../databases/databases.js').db;
module.exports = function viewedVideoSponsorTime(req, res) {
let UUID = req.query.UUID;
if (UUID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//up the view count by one
db.prepare('run', "UPDATE sponsorTimes SET views = views + 1 WHERE UUID = ?", [UUID]);
res.sendStatus(200);
}

View File

@@ -0,0 +1,16 @@
import {db} from '../databases/databases';
import {Request, Response} from 'express';
export function viewedVideoSponsorTime(req: Request, res: Response): Response {
let UUID = req.query.UUID;
if (UUID == undefined) {
//invalid request
return res.sendStatus(400);
}
//up the view count by one
db.prepare('run', "UPDATE sponsorTimes SET views = views + 1 WHERE UUID = ?", [UUID]);
return res.sendStatus(200);
}

View File

@@ -1,47 +1,47 @@
var config = require('../config.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
var isUserTrustworthy = require('../utils/isUserTrustworthy.js');
const { getVoteAuthor, getVoteAuthorRaw, dispatchEvent } = require('../utils/webhookUtils.js');
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var YouTubeAPI = require('../utils/youtubeAPI.js');
var request = require('request');
const logger = require('../utils/logger.js');
const isUserVIP = require('../utils/isUserVIP.js');
import {Request, Response} from 'express';
import {Logger} from '../utils/logger';
import {isUserVIP} from '../utils/isUserVIP';
import request from 'request';
import {YouTubeAPI} from '../utils/youtubeApi';
import {db, privateDB} from '../databases/databases';
import {dispatchEvent, getVoteAuthor, getVoteAuthorRaw} from '../utils/webhookUtils';
import {isUserTrustworthy} from '../utils/isUserTrustworthy';
import {getFormattedTime} from '../utils/getFormattedTime';
import {getIP} from '../utils/getIP';
import {getHash} from '../utils/getHash';
import {config} from '../config';
const voteTypes = {
normal: 0,
incorrect: 1
incorrect: 1,
};
interface VoteData {
UUID: string;
nonAnonUserID: string;
voteTypeEnum: number;
isVIP: boolean;
isOwnSubmission: boolean;
row: {
votes: number;
views: number;
};
category: string;
incrementAmount: number;
oldIncrementAmount: number;
}
/**
* @param {Object} voteData
* @param {string} voteData.UUID
* @param {string} voteData.nonAnonUserID
* @param {number} voteData.voteTypeEnum
* @param {boolean} voteData.isVIP
* @param {boolean} voteData.isOwnSubmission
* @param voteData.row
* @param {string} voteData.category
* @param {number} voteData.incrementAmount
* @param {number} voteData.oldIncrementAmount
*/
function sendWebhooks(voteData) {
let submissionInfoRow = db.prepare('get', "SELECT s.videoID, s.userID, s.startTime, s.endTime, s.category, u.userName, " +
function sendWebhooks(voteData: VoteData) {
const submissionInfoRow = db.prepare('get', "SELECT s.videoID, s.userID, s.startTime, s.endTime, s.category, u.userName, " +
"(select count(1) from sponsorTimes where userID = s.userID) count, " +
"(select count(1) from sponsorTimes where userID = s.userID and votes <= -2) disregarded " +
"FROM sponsorTimes s left join userNames u on s.userID = u.userID where s.UUID=?",
[voteData.UUID]);
[voteData.UUID]);
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [voteData.nonAnonUserID]);
const userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [voteData.nonAnonUserID]);
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
let webhookURL = null;
let webhookURL: string = null;
if (voteData.voteTypeEnum === voteTypes.normal) {
webhookURL = config.discordReportChannelWebhookURL;
} else if (voteData.voteTypeEnum === voteTypes.incorrect) {
@@ -51,20 +51,20 @@ function sendWebhooks(voteData) {
if (config.youtubeAPIKey !== null) {
YouTubeAPI.listVideos(submissionInfoRow.videoID, (err, data) => {
if (err || data.items.length === 0) {
err && logger.error(err);
err && Logger.error(err.toString());
return;
}
let isUpvote = voteData.incrementAmount > 0;
const isUpvote = voteData.incrementAmount > 0;
// Send custom webhooks
dispatchEvent(isUpvote ? "vote.up" : "vote.down", {
"user": {
"status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission)
"status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission),
},
"video": {
"id": submissionInfoRow.videoID,
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID,
"thumbnail": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : ""
"thumbnail": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
},
"submission": {
"UUID": voteData.UUID,
@@ -77,51 +77,51 @@ function sendWebhooks(voteData) {
"username": submissionInfoRow.userName,
"submissions": {
"total": submissionInfoRow.count,
"ignored": submissionInfoRow.disregarded
}
}
"ignored": submissionInfoRow.disregarded,
},
},
},
"votes": {
"before": voteData.row.votes,
"after": (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount)
}
"after": (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount),
},
});
// Send discord message
if (webhookURL !== null && !isUpvote) {
request.post(webhookURL, {
json: {
"embeds": [{
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID
+ "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2),
"description": "**" + voteData.row.votes + " Votes Prior | " +
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
+ " Views**\n\n**Submission ID:** " + voteData.UUID
"description": "**" + voteData.row.votes + " Votes Prior | " +
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
+ " Views**\n\n**Submission ID:** " + voteData.UUID
+ "\n**Category:** " + submissionInfoRow.category
+ "\n\n**Submitted by:** "+submissionInfoRow.userName+"\n " + submissionInfoRow.userID
+ "\n\n**Total User Submissions:** "+submissionInfoRow.count
+ "\n**Ignored User Submissions:** "+submissionInfoRow.disregarded
+"\n\n**Timestamp:** " +
getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime),
+ "\n\n**Submitted by:** " + submissionInfoRow.userName + "\n " + submissionInfoRow.userID
+ "\n\n**Total User Submissions:** " + submissionInfoRow.count
+ "\n**Ignored User Submissions:** " + submissionInfoRow.disregarded
+ "\n\n**Timestamp:** " +
getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime),
"color": 10813440,
"author": {
"name": getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission)
"name": getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission),
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
},
}],
},
}, (err, res) => {
if (err) {
logger.error("Failed to send reported submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
Logger.error("Failed to send reported submission Discord hook.");
Logger.error(JSON.stringify(err));
Logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending reported submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
Logger.error("Error sending reported submission Discord hook");
Logger.error(JSON.stringify(res));
Logger.error("\n");
}
});
}
@@ -131,31 +131,33 @@ function sendWebhooks(voteData) {
}
}
function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
function categoryVote(UUID: string, userID: string, isVIP: boolean, isOwnSubmission: boolean, category: string, hashedIP: string, res: Response) {
// Check if they've already made a vote
let previousVoteInfo = privateDB.prepare('get', "select count(*) as votes, category from categoryVotes where UUID = ? and userID = ?", [UUID, userID]);
const usersLastVoteInfo = privateDB.prepare('get', "select count(*) as votes, category from categoryVotes where UUID = ? and userID = ?", [UUID, userID]);
if (previousVoteInfo !== undefined && previousVoteInfo.category === category) {
if (usersLastVoteInfo?.category === category) {
// Double vote, ignore
res.sendStatus(200);
return;
}
let currentCategory = db.prepare('get', "select category from sponsorTimes where UUID = ?", [UUID]);
const currentCategory = db.prepare('get', "select category from sponsorTimes where UUID = ?", [UUID]);
if (!currentCategory) {
// Submission doesn't exist
res.status("400").send("Submission doesn't exist.");
res.status(400).send("Submission doesn't exist.");
return;
}
if (!config.categoryList.includes(category)) {
res.status("400").send("Category doesn't exist.");
return;
res.status(400).send("Category doesn't exist.");
return;
}
let timeSubmitted = Date.now();
const nextCategoryInfo = db.prepare("get", "select votes from categoryVotes where UUID = ? and category = ?", [UUID, category]);
let voteAmount = isVIP ? 500 : 1;
const timeSubmitted = Date.now();
const voteAmount = isVIP ? 500 : 1;
// Add the vote
if (db.prepare('get', "select count(*) as count from categoryVotes where UUID = ? and category = ?", [UUID, category]).count > 0) {
@@ -167,25 +169,25 @@ function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
}
// Add the info into the private db
if (previousVoteInfo !== undefined) {
if (usersLastVoteInfo?.votes > 0) {
// Reverse the previous vote
db.prepare('run', "update categoryVotes set votes = votes - ? where UUID = ? and category = ?", [voteAmount, UUID, previousVoteInfo.category]);
db.prepare('run', "update categoryVotes set votes = votes - ? where UUID = ? and category = ?", [voteAmount, UUID, usersLastVoteInfo.category]);
privateDB.prepare('run', "update categoryVotes set category = ?, timeSubmitted = ?, hashedIP = ? where userID = ?", [category, timeSubmitted, hashedIP, userID]);
privateDB.prepare('run', "update categoryVotes set category = ?, timeSubmitted = ?, hashedIP = ? where userID = ? and UUID = ?", [category, timeSubmitted, hashedIP, userID, UUID]);
} else {
privateDB.prepare('run', "insert into categoryVotes (UUID, userID, hashedIP, category, timeSubmitted) values (?, ?, ?, ?, ?)", [UUID, userID, hashedIP, category, timeSubmitted]);
}
// See if the submissions category is ready to change
let currentCategoryInfo = db.prepare("get", "select votes from categoryVotes where UUID = ? and category = ?", [UUID, currentCategory.category]);
const currentCategoryInfo = db.prepare("get", "select votes from categoryVotes where UUID = ? and category = ?", [UUID, currentCategory.category]);
let submissionInfo = db.prepare("get", "SELECT userID, timeSubmitted, votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
let isSubmissionVIP = submissionInfo && isUserVIP(submissionInfo.userID);
let startingVotes = isSubmissionVIP ? 10000 : 1;
const submissionInfo = db.prepare("get", "SELECT userID, timeSubmitted, votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
const isSubmissionVIP = submissionInfo && isUserVIP(submissionInfo.userID);
const startingVotes = isSubmissionVIP ? 10000 : 1;
// Change this value from 1 in the future to make it harder to change categories
// Done this way without ORs incase the value is zero
let currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
const currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
// Add submission as vote
if (!currentCategoryInfo && submissionInfo) {
@@ -194,11 +196,11 @@ function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
privateDB.prepare("run", "insert into categoryVotes (UUID, userID, hashedIP, category, timeSubmitted) values (?, ?, ?, ?, ?)", [UUID, submissionInfo.userID, "unknown", currentCategory.category, submissionInfo.timeSubmitted]);
}
let nextCategoryCount = (previousVoteInfo.votes || 0) + voteAmount;
const nextCategoryCount = (nextCategoryInfo?.votes || 0) + voteAmount;
//TODO: In the future, raise this number from zero to make it harder to change categories
// VIPs change it every time
if (nextCategoryCount - currentCategoryCount >= (submissionInfo ? Math.max(Math.ceil(submissionInfo.votes / 2), 1) : 1) || isVIP) {
if (nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2) || isVIP || isOwnSubmission) {
// Replace the category
db.prepare('run', "update sponsorTimes set category = ? where UUID = ?", [category, UUID]);
}
@@ -206,11 +208,11 @@ function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
res.sendStatus(200);
}
async function voteOnSponsorTime(req, res) {
let UUID = req.query.UUID;
let userID = req.query.userID;
let type = req.query.type;
let category = req.query.category;
async function voteOnSponsorTime(req: Request, res: Response) {
const UUID = req.query.UUID as string;
let userID = req.query.userID as string;
let type = req.query.type !== undefined ? parseInt(req.query.type as string) : undefined;
const category = req.query.category as string;
if (UUID === undefined || userID === undefined || (type === undefined && category === undefined)) {
//invalid request
@@ -219,51 +221,63 @@ async function voteOnSponsorTime(req, res) {
}
//hash the userID
let nonAnonUserID = getHash(userID);
const nonAnonUserID = getHash(userID);
userID = getHash(userID + UUID);
//x-forwarded-for if this server is behind a proxy
let ip = getIP(req);
const ip = getIP(req);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(ip + config.globalSalt);
const hashedIP = getHash(ip + config.globalSalt);
//check if this user is on the vip list
let isVIP = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [nonAnonUserID]).userCount > 0;
const isVIP = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [nonAnonUserID]).userCount > 0;
//check if user voting on own submission
let isOwnSubmission = db.prepare("get", "SELECT UUID as submissionCount FROM sponsorTimes where userID = ? AND UUID = ?", [nonAnonUserID, UUID]) !== undefined;
const isOwnSubmission = db.prepare("get", "SELECT UUID as submissionCount FROM sponsorTimes where userID = ? AND UUID = ?", [nonAnonUserID, UUID]) !== undefined;
if (!isVIP) {
const isVideoLocked = !!db.prepare('get', 'SELECT noSegments.category from noSegments left join sponsorTimes' +
' on (noSegments.videoID = sponsorTimes.videoID and noSegments.category = sponsorTimes.category)' +
' where UUID = ?', [UUID]);
if (isVideoLocked) {
res.status(403).send("Not allowed to vote on video that has been locked by a VIP.");
return;
}
}
if (type === undefined && category !== undefined) {
return categoryVote(UUID, nonAnonUserID, isVIP, category, hashedIP, res);
return categoryVote(UUID, nonAnonUserID, isVIP, isOwnSubmission, category, hashedIP, res);
}
if (type == 1 && !isVIP && !isOwnSubmission) {
// Check if upvoting hidden segment
let voteInfo = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
const voteInfo = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (voteInfo && voteInfo.votes <= -2) {
res.status(403).send("Not allowed to upvote segment with too many downvotes unless you are VIP.")
res.status(403).send("Not allowed to upvote segment with too many downvotes unless you are VIP.");
return;
}
}
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
let warningsCount = db.prepare('get', "SELECT count(1) as count FROM warnings WHERE userID = ? AND issueTime > ?",
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))]
const warningsCount = db.prepare('get', "SELECT count(1) as count FROM warnings WHERE userID = ? AND issueTime > ?",
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
).count;
if (warningsCount >= config.maxNumberOfActiveWarnings) {
return res.status(403).send('Vote blocked. Too many active warnings!');
return res.status(403).send('Vote blocked. Too many active warnings!');
}
let voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect;
const voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect;
try {
//check if vote has already happened
let votesRow = privateDB.prepare('get', "SELECT type FROM votes WHERE userID = ? AND UUID = ?", [userID, UUID]);
const votesRow = privateDB.prepare('get', "SELECT type FROM votes WHERE userID = ? AND UUID = ?", [userID, UUID]);
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
let incrementAmount = 0;
let oldIncrementAmount = 0;
@@ -308,12 +322,12 @@ async function voteOnSponsorTime(req, res) {
}
//check if the increment amount should be multiplied (downvotes have more power if there have been many views)
let row = db.prepare('get', "SELECT votes, views FROM sponsorTimes WHERE UUID = ?", [UUID]);
const row = db.prepare('get', "SELECT votes, views FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (voteTypeEnum === voteTypes.normal) {
if ((isVIP || isOwnSubmission) && incrementAmount < 0) {
//this user is a vip and a downvote
incrementAmount = - (row.votes + 2 - oldIncrementAmount);
incrementAmount = -(row.votes + 2 - oldIncrementAmount);
type = incrementAmount;
}
} else if (voteTypeEnum == voteTypes.incorrect) {
@@ -325,10 +339,10 @@ async function voteOnSponsorTime(req, res) {
}
// Only change the database if they have made a submission before and haven't voted recently
let ableToVote = isVIP
|| (db.prepare("get", "SELECT userID FROM sponsorTimes WHERE userID = ?", [nonAnonUserID]) !== undefined
&& privateDB.prepare("get", "SELECT userID FROM shadowBannedUsers WHERE userID = ?", [nonAnonUserID]) === undefined
&& privateDB.prepare("get", "SELECT UUID FROM votes WHERE UUID = ? AND hashedIP = ? AND userID != ?", [UUID, hashedIP, userID]) === undefined);
const ableToVote = isVIP
|| (db.prepare("get", "SELECT userID FROM sponsorTimes WHERE userID = ?", [nonAnonUserID]) !== undefined
&& privateDB.prepare("get", "SELECT userID FROM shadowBannedUsers WHERE userID = ?", [nonAnonUserID]) === undefined
&& privateDB.prepare("get", "SELECT UUID FROM votes WHERE UUID = ? AND hashedIP = ? AND userID != ?", [UUID, hashedIP, userID]) === undefined);
if (ableToVote) {
//update the votes table
@@ -352,21 +366,21 @@ async function voteOnSponsorTime(req, res) {
//for each positive vote, see if a hidden submission can be shown again
if (incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
//find the UUID that submitted the submission that was voted on
let submissionUserIDInfo = db.prepare('get', "SELECT userID FROM sponsorTimes WHERE UUID = ?", [UUID]);
const submissionUserIDInfo = db.prepare('get', "SELECT userID FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (!submissionUserIDInfo) {
// They are voting on a non-existent submission
res.status(400).send("Voting on a non-existent submission");
return;
}
let submissionUserID = submissionUserIDInfo.userID;
const submissionUserID = submissionUserIDInfo.userID;
//check if any submissions are hidden
let hiddenSubmissionsRow = db.prepare('get', "SELECT count(*) as hiddenSubmissions FROM sponsorTimes WHERE userID = ? AND shadowHidden > 0", [submissionUserID]);
const hiddenSubmissionsRow = db.prepare('get', "SELECT count(*) as hiddenSubmissions FROM sponsorTimes WHERE userID = ? AND shadowHidden > 0", [submissionUserID]);
if (hiddenSubmissionsRow.hiddenSubmissions > 0) {
//see if some of this users submissions should be visible again
if (await isUserTrustworthy(submissionUserID)) {
//they are trustworthy again, show 2 of their submissions again, if there are two to show
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE ROWID IN (SELECT ROWID FROM sponsorTimes WHERE userID = ? AND shadowHidden = 1 LIMIT 2)", [submissionUserID]);
@@ -387,17 +401,16 @@ async function voteOnSponsorTime(req, res) {
row,
category,
incrementAmount,
oldIncrementAmount
oldIncrementAmount,
});
}
} catch (err) {
logger.error(err);
Logger.error(err);
res.status(500).json({error: 'Internal error creating segment vote'});
}
}
module.exports = {
voteOnSponsorTime,
endpoint: voteOnSponsorTime
export {
voteOnSponsorTime,
};