Added new submission method

This commit is contained in:
Ajay Ramachandran 2020-04-06 16:43:47 -04:00
parent b74ca3962b
commit bd2f00d5f7
6 changed files with 391 additions and 197 deletions

View file

@ -9,7 +9,8 @@ var loggerMiddleware = require('./middleware/logger.js');
// Routes
var getVideoSponsorTimes = require('./routes/getVideoSponsorTimes.js');
var submitSponsorTimes = require('./routes/submitSponsorTimes.js');
var oldSubmitSponsorTimes = require('./routes/oldSubmitSponsorTimes.js');
var postSkipSegments = require('./routes/postSkipSegments.js');
var voteOnSponsorTime = require('./routes/voteOnSponsorTime.js');
var viewedVideoSponsorTime = require('./routes/viewedVideoSponsorTime.js');
var setUsername = require('./routes/setUsername.js');
@ -26,13 +27,17 @@ var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js');
//setup CORS correctly
app.use(corsMiddleware);
app.use(loggerMiddleware);
app.use(express.json())
//add the get function
app.get('/api/getVideoSponsorTimes', getVideoSponsorTimes);
//add the oldpost function
app.get('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
app.post('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
//add the post function
app.get('/api/postVideoSponsorTimes', submitSponsorTimes);
app.post('/api/postVideoSponsorTimes', submitSponsorTimes);
app.post('/api/skipSegments', postSkipSegments);
//voting endpoint
app.get('/api/voteOnSponsorTime', voteOnSponsorTime);

View file

@ -0,0 +1,9 @@
var config = require('../config.js');
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,212 @@
var config = require('../config.js');
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var YouTubeAPI = require('../utils/youtubeAPI.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
// TODO: might need to be a util
//returns true if the user is considered trustworthy
//this happens after a user has made 5 submissions and has less than 60% downvoted submissions
async function isUserTrustworthy(userID) {
//check to see if this user how many submissions this user has submitted
let totalSubmissionsRow = db.prepare("SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?").get(userID);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
let downvotedSubmissionsRow = db.prepare("SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)").get(userID);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}
function sendDiscordNotification(userID, videoID, UUID, segmentInfo) {
//check if they are a first time user
//if so, send a notification to discord
if (config.youtubeAPIKey !== null && config.discordFirstTimeSubmissionsWebhookURL !== null) {
let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(userID);
// If it is a first time submission
if (userSubmissionCountRow.submissionCount === 0) {
YouTubeAPI.videos.list({
part: "snippet",
id: videoID
}, function (err, data) {
if (err || data.items.length === 0) {
err && console.log(err);
return;
}
request.post(config.discordFirstTimeSubmissionsWebhookURL, {
json: {
"embeds": [{
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (segmentInfo.segment.toFixed(0) - 2),
"description": "Submission ID: " + UUID +
"\n\nTimestamp: " +
getFormattedTime(segmentInfo.segment[0]) + " to " + getFormattedTime(segmentInfo.segment[1]) +
"\n\nCategory: " + segmentInfo.category,
"color": 10813440,
"author": {
"name": userID
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
console.log("Failed to send first time submission Discord hook.");
console.log(JSON.stringify(err));
console.log("\n");
} else if (res && res.statusCode >= 400) {
console.log("Error sending first time submission Discord hook");
console.log(JSON.stringify(res));
console.log("\n");
}
});
});
}
}
}
module.exports = async function postSkipSegments(req, res) {
let videoID = req.query.videoID || req.body.videoID;
let userID = req.query.userID || req.body.userID;
let segments = req.body.segments;
if (segments === undefined) {
// Use query instead
segments = [{
segment: [req.query.startTime, req.query.endTime],
category: req.query.category
}];
}
//check if all correct inputs are here and the length is 1 second or more
if (videoID == undefined || userID == undefined || segments == undefined || segments.length < 1) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
// Check if all submissions are correct
for (let i = 0; i < segments.length; i++) {
if (segments[i] === undefined || segments[i].segment === undefined || segments[i].category === undefined) {
//invalid request
res.sendStatus(400);
return;
}
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
if (Math.abs(startTime - endTime) < 1 || isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime > endTime) {
//invalid request
res.sendStatus(400);
return;
}
//check if this info has already been submitted before
let duplicateCheck2Row =
db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID]);
if (duplicateCheck2Row == null) {
res.sendStatus(409);
return;
}
}
try {
//check if this user is on the vip list
let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(userID);
//get current time
let timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?").get([hashedIP, videoID, yesterday]);
if (rateLimitCheckRow.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
return;
}
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?").get([userID, videoID]);
if (duplicateCheckRow.count >= 8) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
return;
}
//check to see if this user is shadowbanned
let shadowBanRow = privateDB.prepare("SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?").get(userID);
let shadowBanned = shadowBanRow.userCount;
if (!(await isUserTrustworthy(userID))) {
//hide this submission as this user is untrustworthy
shadowBanned = 1;
}
let startingVotes = 0;
if (vipRow.userCount > 0) {
//this user is a vip, start them at a higher approval rating
startingVotes = 10;
}
for (const segmentInfo of segments) {
//this can just be a hash of the data
//it's better than generating an actual UUID like what was used before
//also better for duplication checking
let UUID = getHash("v2-categories" + videoID + segmentInfo.segment[0] +
segmentInfo.segment[1] + segmentInfo.category + userID, 1);
try {
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, segmentInfo.segment[0],
segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned);
//add to private db as well
privateDB.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?)").run(videoID, hashedIP, timeSubmitted);
res.sendStatus(200);
} catch (err) {
//a DB change probably occurred
res.sendStatus(502);
console.log("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
segmentInfo.segment[1] + ", " + userID + ", " + segmentInfo.category);
return;
}
// Discord notification
sendDiscordNotification(userID, videoID, UUID, segmentInfo);
}
} catch (err) {
console.error(err);
res.send(500);
}
}

View file

@ -1,192 +0,0 @@
var config = require('../config.js');
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var YouTubeAPI = require('../utils/youtubeAPI.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
// TODO: might need to be a util
//returns true if the user is considered trustworthy
//this happens after a user has made 5 submissions and has less than 60% downvoted submissions
async function isUserTrustworthy(userID) {
//check to see if this user how many submissions this user has submitted
let totalSubmissionsRow = db.prepare("SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?").get(userID);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
let downvotedSubmissionsRow = db.prepare("SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)").get(userID);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}
module.exports = async function submitSponsorTimes(req, res) {
let videoID = req.query.videoID;
let startTime = req.query.startTime;
let endTime = req.query.endTime;
let userID = req.query.userID;
//check if all correct inputs are here and the length is 1 second or more
if (videoID == undefined || startTime == undefined || endTime == undefined || userID == undefined
|| Math.abs(startTime - endTime) < 1) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
startTime = parseFloat(startTime);
endTime = parseFloat(endTime);
if (isNaN(startTime) || isNaN(endTime)) {
//invalid request
res.sendStatus(400);
return;
}
if (startTime === Infinity || endTime === Infinity) {
//invalid request
res.sendStatus(400);
return;
}
if (startTime > endTime) {
//time can't go backwards
res.sendStatus(400);
return;
}
try {
//check if this user is on the vip list
let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(userID);
//this can just be a hash of the data
//it's better than generating an actual UUID like what was used before
//also better for duplication checking
let UUID = getHash(videoID + startTime + endTime + userID, 1);
//get current time
let timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?").get([hashedIP, videoID, yesterday]);
if (rateLimitCheckRow.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
} else {
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?").get([userID, videoID]);
if (duplicateCheckRow.count >= 8) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
} else {
//check if this info has already been submitted first
let duplicateCheck2Row = db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID]);
//check to see if this user is shadowbanned
let shadowBanRow = privateDB.prepare("SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?").get(userID);
let shadowBanned = shadowBanRow.userCount;
if (!(await isUserTrustworthy(userID))) {
//hide this submission as this user is untrustworthy
shadowBanned = 1;
}
let startingVotes = 0;
if (vipRow.userCount > 0) {
//this user is a vip, start them at a higher approval rating
startingVotes = 10;
}
if (duplicateCheck2Row == null) {
//not a duplicate, execute query
try {
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, startTime, endTime, startingVotes, UUID, userID, timeSubmitted, 0, shadowBanned);
//add to private db as well
privateDB.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?)").run(videoID, hashedIP, timeSubmitted);
res.sendStatus(200);
} catch (err) {
//a DB change probably occurred
res.sendStatus(502);
console.log("Error when putting sponsorTime in the DB: " + videoID + ", " + startTime + ", " + "endTime" + ", " + userID);
return;
}
} else {
res.sendStatus(409);
}
//check if they are a first time user
//if so, send a notification to discord
if (config.youtubeAPIKey !== null && config.discordFirstTimeSubmissionsWebhookURL !== null && duplicateCheck2Row == null) {
let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(userID);
// If it is a first time submission
if (userSubmissionCountRow.submissionCount === 0) {
YouTubeAPI.videos.list({
part: "snippet",
id: videoID
}, function (err, data) {
if (err || data.items.length === 0) {
err && console.log(err);
return;
}
request.post(config.discordFirstTimeSubmissionsWebhookURL, {
json: {
"embeds": [{
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (startTime.toFixed(0) - 2),
"description": "Submission ID: " + UUID +
"\n\nTimestamp: " +
getFormattedTime(startTime) + " to " + getFormattedTime(endTime),
"color": 10813440,
"author": {
"name": userID
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
console.log("Failed to send first time submission Discord hook.");
console.log(JSON.stringify(err));
console.log("\n");
} else if (res && res.statusCode >= 400) {
console.log("Error sending first time submission Discord hook");
console.log(JSON.stringify(res));
console.log("\n");
}
});
});
}
}
}
}
} catch (err) {
console.error(err);
res.send(500);
}
}

View file

@ -3,8 +3,8 @@ var request = require('request');
var utils = require('../utils.js');
describe('postVideoSponsorTime', () => {
it('Should be able to create a time', (done) => {
describe('postVideoSponsorTime (Old submission method)', () => {
it('Should be able to submit a time (GET)', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=djgofQKWmXc&startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
@ -14,6 +14,16 @@ describe('postVideoSponsorTime', () => {
});
});
it('Should be able to submit a time (POST)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=djgofQKWmXc&startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should return 400 for missing params', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=1&endTime=10&userID=test", null,

View file

@ -0,0 +1,150 @@
var assert = require('assert');
var request = require('request');
var utils = require('../utils.js');
describe('postSkipSegments', () => {
it('Should be able to submit a single time (Params method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=djgofQKWmXc&startTime=1&endTime=10&userID=test&category=sponsor", null,
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should be able to submit a single time (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
videoID: "djgofQKWmXc",
segments: [{
segment: [0, 10],
category: "sponsor"
}]
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should be able to submit multiple times (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
videoID: "djgofQKWmXc",
segments: [{
segment: [0, 10],
category: "sponsor"
}, {
segment: [30, 60],
category: "intro"
}]
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should return 400 for missing params (Params method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(false);
if (res.statusCode === 400) done();
else done(false);
});
});
it('Should return 400 for missing params (JSON method) 1', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
segments: [{
segment: [0, 10],
category: "sponsor"
}, {
segment: [30, 60],
category: "intro"
}]
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should return 400 for missing params (JSON method) 2', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
videoID: "djgofQKWmXc"
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should return 400 for missing params (JSON method) 3', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
videoID: "djgofQKWmXc",
segments: [{
segment: [0],
category: "sponsor"
}, {
segment: [30, 60],
category: "intro"
}]
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should return 400 for missing params (JSON method) 4', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
videoID: "djgofQKWmXc",
segments: [{
segment: [0, 10]
}, {
segment: [30, 60],
category: "intro"
}]
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
it('Should return 400 for missing params (JSON method) 5', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", JSON.stringify({
body: {
videoID: "djgofQKWmXc"
}
}),
(err, res, body) => {
if (err) done(false);
else if (res.statusCode === 200) done();
else done(false);
});
});
});