diff --git a/src/app.js b/src/app.js index 5924715..fa29c38 100644 --- a/src/app.js +++ b/src/app.js @@ -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); diff --git a/src/routes/oldSubmitSponsorTimes.js b/src/routes/oldSubmitSponsorTimes.js new file mode 100644 index 0000000..b818a24 --- /dev/null +++ b/src/routes/oldSubmitSponsorTimes.js @@ -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); +} diff --git a/src/routes/postSkipSegments.js b/src/routes/postSkipSegments.js new file mode 100644 index 0000000..6562d94 --- /dev/null +++ b/src/routes/postSkipSegments.js @@ -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); + } +} diff --git a/src/routes/submitSponsorTimes.js b/src/routes/submitSponsorTimes.js deleted file mode 100644 index a550f00..0000000 --- a/src/routes/submitSponsorTimes.js +++ /dev/null @@ -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); - } -} diff --git a/test/cases/submitSponsorTimes.js b/test/cases/oldSubmitSponsorTimes.js similarity index 60% rename from test/cases/submitSponsorTimes.js rename to test/cases/oldSubmitSponsorTimes.js index 181b550..4c0b8a1 100644 --- a/test/cases/submitSponsorTimes.js +++ b/test/cases/oldSubmitSponsorTimes.js @@ -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, diff --git a/test/cases/postSkipSegments.js b/test/cases/postSkipSegments.js new file mode 100644 index 0000000..e058466 --- /dev/null +++ b/test/cases/postSkipSegments.js @@ -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); + }); + }); +}); \ No newline at end of file