From 43db13ab3fa30a69770e422d3f9e2794007a018a Mon Sep 17 00:00:00 2001 From: Nanobyte Date: Mon, 14 Sep 2020 09:17:35 +0200 Subject: [PATCH 1/5] Added getUserInfo endpoint This endpoint provides the following data about the user: * userID * userName * minutesSaved * segmentCount * viewCount --- src/app.js | 3 ++ src/routes/getUserInfo.js | 71 +++++++++++++++++++++++++++ test/cases/getUserInfo.js | 101 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 src/routes/getUserInfo.js create mode 100644 test/cases/getUserInfo.js diff --git a/src/app.js b/src/app.js index c70115d..c6ab3b5 100644 --- a/src/app.js +++ b/src/app.js @@ -22,6 +22,7 @@ var getViewsForUser = require('./routes/getViewsForUser.js'); var getTopUsers = require('./routes/getTopUsers.js'); var getTotalStats = require('./routes/getTotalStats.js'); var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js'); +var getUserInfo = require('./routes/getUserInfo.js'); // Old Routes var oldGetVideoSponsorTimes = require('./routes/oldGetVideoSponsorTimes.js'); @@ -86,6 +87,8 @@ app.get('/api/getTopUsers', getTopUsers); //send the total submissions, total views and total minutes saved app.get('/api/getTotalStats', getTotalStats); +app.get('/api/getUserInfo', getUserInfo); + //send out a formatted time saved total app.get('/api/getDaysSavedFormatted', getDaysSavedFormatted); diff --git a/src/routes/getUserInfo.js b/src/routes/getUserInfo.js new file mode 100644 index 0000000..8479129 --- /dev/null +++ b/src/routes/getUserInfo.js @@ -0,0 +1,71 @@ +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; + } +} + +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), + }); +} diff --git a/test/cases/getUserInfo.js b/test/cases/getUserInfo.js new file mode 100644 index 0000000..499ae3c --- /dev/null +++ b/test/cases/getUserInfo.js @@ -0,0 +1,101 @@ +var request = require('request'); +var utils = require('../utils.js'); +var db = require('../../src/databases/databases.js').db; +var getHash = require('../../src/utils/getHash.js'); + +describe('getUserInfo', () => { + before(() => { + let startOfUserNamesQuery = "INSERT INTO userNames (userID, userName) VALUES"; + db.exec(startOfUserNamesQuery + "('" + getHash("getuserinfo_user_01") + "', 'Username user 01')"); + let startOfSponsorTimesQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden) VALUES"; + db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000001', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 0)"); + db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000002', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 0)"); + db.exec(startOfSponsorTimesQuery + "('yyyxxxzzz', 1, 11, -1, 'uuid000003', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 0)"); + db.exec(startOfSponsorTimesQuery + "('yyyxxxzzz', 1, 11, -2, 'uuid000004', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 1)"); + db.exec(startOfSponsorTimesQuery + "('xzzzxxyyy', 1, 11, -5, 'uuid000005', '" + getHash("getuserinfo_user_01") + "', 0, 10, 'sponsor', 1)"); + db.exec(startOfSponsorTimesQuery + "('zzzxxxyyy', 1, 11, 2, 'uuid000006', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 0)"); + db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000007', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 1)"); + db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000008', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 1)"); + }); + + it('Should be able to get a 200', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo?userID=getuserinfo_user_01', null, + (err, res, body) => { + if (err) { + done('couldn\'t call endpoint'); + } else { + if (res.statusCode !== 200) { + done('non 200'); + } else { + done(); // pass + } + } + }); + }); + + it('Should be able to get a 400 (No userID parameter)', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo', null, + (err, res, body) => { + if (err) { + done('couldn\'t call endpoint'); + } else { + if (res.statusCode !== 400) { + done('non 400'); + } else { + done(); // pass + } + } + }); + }); + + it('Should return info', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo?userID=getuserinfo_user_01', null, + (err, res, body) => { + if (err) { + done("couldn't call endpoint"); + } else { + if (res.statusCode !== 200) { + done("non 200"); + } else { + const data = JSON.parse(body); + if (data.userName !== 'Username user 01') { + return done('Returned incorrect userName "' + data.userName + '"'); + } + if (data.minutesSaved !== 5) { + return done('Returned incorrect minutesSaved "' + data.minutesSaved + '"'); + } + if (data.viewCount !== 30) { + return done('Returned incorrect viewCount "' + data.viewCount + '"'); + } + if (data.segmentCount !== 3) { + return done('Returned incorrect segmentCount "' + data.segmentCount + '"'); + } + done(); // pass + } + } + }); + }); + + it('Should return userID for userName (No userName set)', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo?userID=getuserinfo_user_02', null, + (err, res, body) => { + if (err) { + done('couldn\'t call endpoint'); + } else { + if (res.statusCode !== 200) { + done('non 200'); + } else { + const data = JSON.parse(body); + if (data.userName !== 'c2a28fd225e88f74945794ae85aef96001d4a1aaa1022c656f0dd48ac0a3ea0f') { + return done('Did not return userID for userName'); + } + done(); // pass + } + } + }); + }); +}); From 075cb9d5f2a28d39aa9f558be343000925e1d1c1 Mon Sep 17 00:00:00 2001 From: Joe Dowd Date: Tue, 15 Sep 2020 23:56:10 +0100 Subject: [PATCH 2/5] added warning system Co-authored-by: Ajay Ramachandran --- databases/_upgrade_sponsorTimes_4.sql | 12 ++++ package-lock.json | 5 ++ src/app.js | 4 ++ src/routes/getUserInfo.js | 11 ++++ src/routes/postWarning.js | 24 ++++++++ src/routes/voteOnSponsorTime.js | 3 + test/cases/getUserInfo.js | 84 ++++++++++++++++++++++++--- test/cases/postWarning.js | 53 +++++++++++++++++ 8 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 databases/_upgrade_sponsorTimes_4.sql create mode 100644 src/routes/postWarning.js create mode 100644 test/cases/postWarning.js diff --git a/databases/_upgrade_sponsorTimes_4.sql b/databases/_upgrade_sponsorTimes_4.sql new file mode 100644 index 0000000..91e51e2 --- /dev/null +++ b/databases/_upgrade_sponsorTimes_4.sql @@ -0,0 +1,12 @@ +BEGIN TRANSACTION; + +/* Create warnings table */ +CREATE TABLE "warnings" ( + userID TEXT NOT NULL, + issueTime INTEGER NOT NULL, + issuerUserID TEXT NOT NULL +); + +UPDATE config SET value = 4 WHERE key = "version"; + +COMMIT; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0878451..5f08ab3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1866,6 +1866,11 @@ "semver": "^5.7.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-forge": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", diff --git a/src/app.js b/src/app.js index 787f32e..7e14923 100644 --- a/src/app.js +++ b/src/app.js @@ -27,6 +27,7 @@ var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js'); var getUserInfo = require('./routes/getUserInfo.js'); var postNoSegments = require('./routes/postNoSegments.js'); var getIsUserVIP = require('./routes/getIsUserVIP.js'); +var warnUser = require('./routes/postWarning.js'); // Old Routes var oldGetVideoSponsorTimes = require('./routes/oldGetVideoSponsorTimes.js'); @@ -105,6 +106,9 @@ app.post('/api/noSegments', postNoSegments); //get if user is a vip app.get('/api/isUserVIP', getIsUserVIP); +//sent user a warning +app.post('/api/warnUser', warnUser); + app.get('/database.db', function (req, res) { res.sendFile("./databases/sponsorTimes.db", { root: "./" }); diff --git a/src/routes/getUserInfo.js b/src/routes/getUserInfo.js index 8479129..de17999 100644 --- a/src/routes/getUserInfo.js +++ b/src/routes/getUserInfo.js @@ -48,6 +48,16 @@ function dbGetViewsForUser (userID) { } } +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; @@ -67,5 +77,6 @@ module.exports = function getUserInfo (req, res) { minutesSaved: segmentsSummary.minutesSaved, segmentCount: segmentsSummary.segmentCount, viewCount: dbGetViewsForUser(userID), + warnings: dbGetWarningsForUser(userID) }); } diff --git a/src/routes/postWarning.js b/src/routes/postWarning.js new file mode 100644 index 0000000..797d307 --- /dev/null +++ b/src/routes/postWarning.js @@ -0,0 +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'); + +module.exports = (req, res) => { + // Collect user input data + let issuerUserID = getHash(req.body.issuerUserID); + let userID = getHash(req.body.userID); + let issueTime = new Date().getTime(); + + // Ensure user is a VIP + if (!isUserVIP(issuerUserID)) { + 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 + "'." + }); + +}; \ No newline at end of file diff --git a/src/routes/voteOnSponsorTime.js b/src/routes/voteOnSponsorTime.js index e74dd3e..e8c4edc 100644 --- a/src/routes/voteOnSponsorTime.js +++ b/src/routes/voteOnSponsorTime.js @@ -270,6 +270,9 @@ async function voteOnSponsorTime(req, res) { } else if (votesRow.type === 2) { //extra downvote oldIncrementAmount = -4; + } else if (votesRow.type === 20) { + //undo/cancel vote + oldIncrementAmount = 0; } else if (votesRow.type < 0) { //vip downvote oldIncrementAmount = votesRow.type; diff --git a/test/cases/getUserInfo.js b/test/cases/getUserInfo.js index 499ae3c..f04c02f 100644 --- a/test/cases/getUserInfo.js +++ b/test/cases/getUserInfo.js @@ -16,6 +16,11 @@ describe('getUserInfo', () => { db.exec(startOfSponsorTimesQuery + "('zzzxxxyyy', 1, 11, 2, 'uuid000006', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 0)"); db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000007', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 1)"); db.exec(startOfSponsorTimesQuery + "('xxxyyyzzz', 1, 11, 2, 'uuid000008', '" + getHash("getuserinfo_user_02") + "', 0, 10, 'sponsor', 1)"); + + + db.exec("INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES ('" + getHash('getuserinfo_warning_0') + "', 10, 'getuserinfo_vip')"); + db.exec("INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES ('" + getHash('getuserinfo_warning_1') + "', 10, 'getuserinfo_vip')"); + db.exec("INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES ('" + getHash('getuserinfo_warning_1') + "', 10, 'getuserinfo_vip')"); }); it('Should be able to get a 200', (done) => { @@ -26,7 +31,7 @@ describe('getUserInfo', () => { done('couldn\'t call endpoint'); } else { if (res.statusCode !== 200) { - done('non 200'); + done('non 200 (' + res.statusCode + ')'); } else { done(); // pass } @@ -62,18 +67,79 @@ describe('getUserInfo', () => { } else { const data = JSON.parse(body); if (data.userName !== 'Username user 01') { - return done('Returned incorrect userName "' + data.userName + '"'); + done('Returned incorrect userName "' + data.userName + '"'); + } else if (data.minutesSaved !== 5) { + done('Returned incorrect minutesSaved "' + data.minutesSaved + '"'); + } else if (data.viewCount !== 30) { + done('Returned incorrect viewCount "' + data.viewCount + '"'); + } else if (data.segmentCount !== 3) { + done('Returned incorrect segmentCount "' + data.segmentCount + '"'); + } else { + done(); // pass } - if (data.minutesSaved !== 5) { - return done('Returned incorrect minutesSaved "' + data.minutesSaved + '"'); + } + } + }); + }); + + it('Should get warning data', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo?userID=getuserinfo_warning_0', null, + (err, res, body) => { + if (err) { + done("couldn't call endpoint"); + } else { + if (res.statusCode !== 200) { + done("non 200"); + } else { + const data = JSON.parse(body); + if (data.warnings !== 1) { + done('wrong number of warnings: ' + data.warnings + ', not ' + 1); + } else { + done(); // pass } - if (data.viewCount !== 30) { - return done('Returned incorrect viewCount "' + data.viewCount + '"'); + } + } + }); + }); + + it('Should get multiple warnings', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo?userID=getuserinfo_warning_1', null, + (err, res, body) => { + if (err) { + done("couldn't call endpoint"); + } else { + if (res.statusCode !== 200) { + done("non 200"); + } else { + const data = JSON.parse(body); + if (data.warnings !== 2) { + done('wrong number of warnings: ' + data.warnings + ', not ' + 2); + } else { + done(); // pass } - if (data.segmentCount !== 3) { - return done('Returned incorrect segmentCount "' + data.segmentCount + '"'); + } + } + }); + }); + + it('Should not get warnings if noe', (done) => { + request.get(utils.getbaseURL() + + '/api/getUserInfo?userID=getuserinfo_warning_2', null, + (err, res, body) => { + if (err) { + done("couldn't call endpoint"); + } else { + if (res.statusCode !== 200) { + done("non 200"); + } else { + const data = JSON.parse(body); + if (data.warnings !== 0) { + done('wrong number of warnings: ' + data.warnings + ', not ' + 0); + } else { + done(); // pass } - done(); // pass } } }); diff --git a/test/cases/postWarning.js b/test/cases/postWarning.js new file mode 100644 index 0000000..7661bf5 --- /dev/null +++ b/test/cases/postWarning.js @@ -0,0 +1,53 @@ +var request = require('request'); +var utils = require('../utils.js'); +var db = require('../../src/databases/databases.js').db; +var getHash = require('../../src/utils/getHash.js'); + +describe('postWarning', () => { + before(() => { + db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("warning-vip") + "')"); + }); + + it('Should update the database version when starting the application', (done) => { + let version = db.prepare('get', 'SELECT key, value FROM config where key = ?', ['version']).value; + if (version > 3) done(); + else done('Version isn\'t greater than 3. Version is ' + version); + }); + + it('Should be able to create warning if vip (exp 200)', (done) => { + let json = { + issuerUserID: 'warning-vip', + userID: 'warning-0' + }; + + request.post(utils.getbaseURL() + + "/api/warnUser", {json}, + (err, res, body) => { + if (err) done(err); + else if (res.statusCode === 200) { + done(); + } else { + console.log(body); + done("Status code was " + res.statusCode); + } + }); + }); + it('Should not be able to create warning if vip (exp 403)', (done) => { + let json = { + issuerUserID: 'warning-not-vip', + userID: 'warning-1' + }; + + request.post(utils.getbaseURL() + + "/api/warnUser", {json}, + (err, res, body) => { + if (err) done(err); + else if (res.statusCode === 403) { + done(); + } else { + console.log(body); + done("Status code was " + res.statusCode); + } + }); + }); +}); \ No newline at end of file From 3c79c0f7a840ac8ec8800dac066c7d7decaae409 Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Wed, 16 Sep 2020 13:48:54 -0400 Subject: [PATCH 3/5] Remove extra test case --- test/cases/postWarning.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/cases/postWarning.js b/test/cases/postWarning.js index 7661bf5..e5e8586 100644 --- a/test/cases/postWarning.js +++ b/test/cases/postWarning.js @@ -8,12 +8,6 @@ describe('postWarning', () => { db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("warning-vip") + "')"); }); - it('Should update the database version when starting the application', (done) => { - let version = db.prepare('get', 'SELECT key, value FROM config where key = ?', ['version']).value; - if (version > 3) done(); - else done('Version isn\'t greater than 3. Version is ' + version); - }); - it('Should be able to create warning if vip (exp 200)', (done) => { let json = { issuerUserID: 'warning-vip', @@ -50,4 +44,4 @@ describe('postWarning', () => { } }); }); -}); \ No newline at end of file +}); From 36f654f41c549d71eb32f44e894beab879d9efae Mon Sep 17 00:00:00 2001 From: Nanobyte Date: Wed, 16 Sep 2020 22:40:11 +0200 Subject: [PATCH 4/5] Blocking users with too many active warnings from submitting votes and submissions --- config.json.example | 4 ++- src/config.js | 4 ++- src/routes/postSkipSegments.js | 10 ++++++ src/routes/voteOnSponsorTime.js | 10 ++++++ test.json | 4 ++- test/cases/postSkipSegments.js | 64 +++++++++++++++++++++++++++++++++ test/cases/voteOnSponsorTime.js | 32 +++++++++++++++++ 7 files changed, 125 insertions(+), 3 deletions(-) diff --git a/config.json.example b/config.json.example index a2eee23..f3a2413 100644 --- a/config.json.example +++ b/config.json.example @@ -22,5 +22,7 @@ "mode": "development", "readOnly": false, "webhooks": [], - "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"] // List of supported categories any other category will be rejected + "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"], // List of supported categories any other category will be rejected + "maxNumberOfActiveWarnings": 3, // Users with this number of warnings will be blocked until warnings expire + "hoursAfterWarningExpire": 24 } diff --git a/src/config.js b/src/config.js index d85b930..b3f30d3 100644 --- a/src/config.js +++ b/src/config.js @@ -20,7 +20,9 @@ addDefaults(config, { "privateDBSchema": "./databases/_private.db.sql", "readOnly": false, "webhooks": [], - "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"] + "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"], + "maxNumberOfActiveWarnings": 3, + "hoursAfterWarningExpires": 24 }) module.exports = config; diff --git a/src/routes/postSkipSegments.js b/src/routes/postSkipSegments.js index 820e0cf..4961e92 100644 --- a/src/routes/postSkipSegments.js +++ b/src/routes/postSkipSegments.js @@ -269,6 +269,16 @@ module.exports = async function postSkipSegments(req, res) { //hash the ip 5000 times so no one can get it from the database let 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))] + ).count; + + if (warningsCount >= config.maxNumberOfActiveWarnings) { + 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 }); diff --git a/src/routes/voteOnSponsorTime.js b/src/routes/voteOnSponsorTime.js index e8c4edc..0e6de00 100644 --- a/src/routes/voteOnSponsorTime.js +++ b/src/routes/voteOnSponsorTime.js @@ -235,6 +235,16 @@ async function voteOnSponsorTime(req, res) { 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))] + ).count; + + if (warningsCount >= config.maxNumberOfActiveWarnings) { + return res.status(403).send('Vote blocked. Too many active warnings!'); + } let voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect; diff --git a/test.json b/test.json index 8905d83..c620be4 100644 --- a/test.json +++ b/test.json @@ -49,5 +49,7 @@ ] } ], - "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"] + "categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"], + "maxNumberOfActiveWarnings": 3, + "hoursAfterWarningExpires": 24 } diff --git a/test/cases/postSkipSegments.js b/test/cases/postSkipSegments.js index a2e2c97..b8966b1 100644 --- a/test/cases/postSkipSegments.js +++ b/test/cases/postSkipSegments.js @@ -1,5 +1,7 @@ var assert = require('assert'); var request = require('request'); +var config = require('../../src/config.js'); +var getHash = require('../../src/utils/getHash.js'); var utils = require('../utils.js'); @@ -7,6 +9,24 @@ var databases = require('../../src/databases/databases.js'); var db = databases.db; describe('postSkipSegments', () => { + before(() => { + const now = Date.now(); + const warnVip01Hash = getHash("warn-vip01"); + const warnUser01Hash = getHash("warn-user01"); + const warnUser02Hash = getHash("warn-user02"); + const MILLISECONDS_IN_HOUR = 3600000; + const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires; + const startOfWarningQuery = 'INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES'; + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + now + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-1000) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-2000) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-3601000) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 1000)) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 2000)) + "', '" + warnVip01Hash + "')"); + }); + it('Should be able to submit a single time (Params method)', (done) => { request.post(utils.getbaseURL() + "/api/postVideoSponsorTimes?videoID=dQw4w9WgXcR&startTime=2&endTime=10&userID=test&category=sponsor", null, @@ -130,6 +150,50 @@ describe('postSkipSegments', () => { }); }); + it('Should be rejected if user has to many active warnings', (done) => { + request.post(utils.getbaseURL() + + "/api/postVideoSponsorTimes", { + json: { + userID: "warn-user01", + videoID: "dQw4w9WgXcF", + segments: [{ + segment: [0, 10], + category: "sponsor" + }] + } + }, + (err, res, body) => { + if (err) done(err); + else if (res.statusCode === 403) { + done(); // success + } else { + done("Status code was " + res.statusCode); + } + }); + }); + + it('Should be accepted if user has some active warnings', (done) => { + request.post(utils.getbaseURL() + + "/api/postVideoSponsorTimes", { + json: { + userID: "warn-user02", + videoID: "dQw4w9WgXcF", + segments: [{ + segment: [50, 60], + category: "sponsor" + }] + } + }, + (err, res, body) => { + if (err) done(err); + else if (res.statusCode === 200) { + done(); // success + } else { + done("Status code was " + res.statusCode + " " + body); + } + }); + }); + it('Should be allowed if youtube thinks duration is 0', (done) => { request.get(utils.getbaseURL() + "/api/postVideoSponsorTimes?videoID=noDuration&startTime=30&endTime=10000&userID=testing", null, diff --git a/test/cases/voteOnSponsorTime.js b/test/cases/voteOnSponsorTime.js index 2e94a0b..084d0f9 100644 --- a/test/cases/voteOnSponsorTime.js +++ b/test/cases/voteOnSponsorTime.js @@ -1,11 +1,19 @@ const request = require('request'); +const config = require('../../src/config.js'); const { db, privateDB } = require('../../src/databases/databases.js'); const utils = require('../utils.js'); const getHash = require('../../src/utils/getHash.js'); describe('voteOnSponsorTime', () => { before(() => { + const now = Date.now(); + const warnVip01Hash = getHash("warn-vip01"); + const warnUser01Hash = getHash("warn-voteuser01"); + const warnUser02Hash = getHash("warn-voteuser02"); + const MILLISECONDS_IN_HOUR = 3600000; + const warningExpireTime = MILLISECONDS_IN_HOUR * config.hoursAfterWarningExpires; let startOfQuery = "INSERT INTO sponsorTimes (videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) VALUES"; + const startOfWarningQuery = 'INSERT INTO warnings (userID, issueTime, issuerUserID) VALUES'; db.exec(startOfQuery + "('vote-testtesttest', 1, 11, 2, 'vote-uuid-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest', 1) + "')"); db.exec(startOfQuery + "('vote-testtesttest2', 1, 11, 2, 'vote-uuid-1', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest2', 1) + "')"); @@ -25,6 +33,17 @@ describe('voteOnSponsorTime', () => { db.exec(startOfQuery + "('not-own-submission-video', 1, 11, 500, 'not-own-submission-uuid', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0, '" + getHash('not-own-submission-video', 1) + "')"); db.exec(startOfQuery + "('incorrect-category', 1, 11, 500, 'incorrect-category', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0, '" + getHash('incorrect-category', 1) + "')"); db.exec(startOfQuery + "('incorrect-category-change', 1, 11, 500, 'incorrect-category-change', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0, '" + getHash('incorrect-category-change', 1) + "')"); + db.exec(startOfQuery + "('vote-testtesttest', 1, 11, 2, 'warnvote-uuid-0', 'testman', 0, 50, 'sponsor', 0, '" + getHash('vote-testtesttest', 1) + "')"); + + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + now + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-1000) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-2000) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser01Hash + "', '" + (now-3601000) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + now + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 1000)) + "', '" + warnVip01Hash + "')"); + db.exec(startOfWarningQuery + "('" + warnUser02Hash + "', '" + (now-(warningExpireTime + 2000)) + "', '" + warnVip01Hash + "')"); + db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("VIPUser") + "')"); privateDB.exec("INSERT INTO shadowBannedUsers (userID) VALUES ('" + getHash("randomID4") + "')"); @@ -332,4 +351,17 @@ describe('voteOnSponsorTime', () => { }); }); + it('Should not be able to upvote a segment (Too many warning)', (done) => { + request.get(utils.getbaseURL() + + "/api/voteOnSponsorTime?userID=warn-voteuser01&UUID=warnvote-uuid-0&type=1", null, + (err, res, body) => { + if (err) done(err); + else if (res.statusCode === 403) { + done(); // success + } else { + done("Status code was " + res.statusCode); + } + }); + }); + }); \ No newline at end of file From 8e33fdf49f6954db690b3a61bd11d64e4df6bb69 Mon Sep 17 00:00:00 2001 From: Nanobyte Date: Sun, 4 Oct 2020 21:37:35 +0200 Subject: [PATCH 5/5] Added segmentShift See #151 --- src/app.js | 4 + src/routes/postSegmentShift.js | 101 ++++++++++++ test/cases/segmentShift.js | 273 +++++++++++++++++++++++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 src/routes/postSegmentShift.js create mode 100644 test/cases/segmentShift.js diff --git a/src/app.js b/src/app.js index a102a76..854031a 100644 --- a/src/app.js +++ b/src/app.js @@ -26,6 +26,7 @@ var getTotalStats = require('./routes/getTotalStats.js'); var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js'); var postNoSegments = require('./routes/postNoSegments.js'); var getIsUserVIP = require('./routes/getIsUserVIP.js'); +var postSegmentShift = require('./routes/postSegmentShift.js'); // Old Routes var oldGetVideoSponsorTimes = require('./routes/oldGetVideoSponsorTimes.js'); @@ -102,6 +103,9 @@ app.post('/api/noSegments', postNoSegments); //get if user is a vip app.get('/api/isUserVIP', getIsUserVIP); +//get if user is a vip +app.post('/api/segmentShift', postSegmentShift); + app.get('/database.db', function (req, res) { res.sendFile("./databases/sponsorTimes.db", { root: "./" }); diff --git a/src/routes/postSegmentShift.js b/src/routes/postSegmentShift.js new file mode 100644 index 0000000..fd8c568 --- /dev/null +++ b/src/routes/postSegmentShift.js @@ -0,0 +1,101 @@ +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); +}; diff --git a/test/cases/segmentShift.js b/test/cases/segmentShift.js new file mode 100644 index 0000000..e1011e2 --- /dev/null +++ b/test/cases/segmentShift.js @@ -0,0 +1,273 @@ +const request = require('request'); +const utils = require('../utils.js'); +const { db } = require('../../src/databases/databases.js'); +const getHash = require('../../src/utils/getHash.js'); + +function dbSponsorTimesAdd(db, videoID, startTime, endTime, UUID, category) { + const votes = 0, + userID = 0, + timeSubmitted = 0, + views = 0, + shadowHidden = 0, + hashedVideoID = `hash_${UUID}`; + db.exec(`INSERT INTO + sponsorTimes (videoID, startTime, endTime, votes, UUID, + userID, timeSubmitted, views, category, shadowHidden, hashedVideoID) + VALUES + ('${videoID}', ${startTime}, ${endTime}, ${votes}, '${UUID}', + '${userID}', ${timeSubmitted}, ${views}, '${category}', ${shadowHidden}, '${hashedVideoID}') + `); +} + +function dbSponsorTimesSetByUUID(db, UUID, startTime, endTime) { + db.prepare('run', `UPDATE sponsorTimes SET startTime = ?, endTime = ? WHERE UUID = ?`, [startTime, endTime, UUID]); +} + +function dbSponsorTimesCompareExpect(db, expect) { + for (let i=0, len=expect.length; i { + if (err) return done(err); + return done(res.statusCode === 403 ? undefined : res.statusCode); + }); + }); + + it('Shift is outside segments', function(done) { + request.post(`${baseURL}/api/segmentShift`, { + json: { + videoID: 'vsegshift01', + userID: privateVipUserID, + startTime: 20, + endTime: 30, + } + }, (err, res, body) => { + if (err) return done(err); + if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`); + const expect = [ + { + UUID: 'vsegshifttest01uuid01', + startTime: 0, + endTime: 10, + }, + { + UUID: 'vsegshifttest01uuid02', + startTime: 50, + endTime: 80, + }, + { + UUID: 'vsegshifttest01uuid03', + startTime: 30, + endTime: 35, + }, + { + UUID: 'vsegshifttest01uuid04', + startTime: 110, + endTime: 130, + }, + ]; + done(dbSponsorTimesCompareExpect(db, expect)); + }); + }); + + it('Shift is inside segment', function(done) { + request.post(`${baseURL}/api/segmentShift`, { + json: { + videoID: 'vsegshift01', + userID: privateVipUserID, + startTime: 65, + endTime: 75, + } + }, (err, res, body) => { + if (err) return done(err); + if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`); + const expect = [ + { + UUID: 'vsegshifttest01uuid01', + startTime: 0, + endTime: 10, + }, + { + UUID: 'vsegshifttest01uuid02', + startTime: 60, + endTime: 80, + }, + { + UUID: 'vsegshifttest01uuid03', + startTime: 40, + endTime: 45, + }, + { + UUID: 'vsegshifttest01uuid04', + startTime: 110, + endTime: 130, + }, + ]; + done(dbSponsorTimesCompareExpect(db, expect)); + }); + }); + + it('Shift is overlaping startTime of segment', function(done) { + request.post(`${baseURL}/api/segmentShift`, { + json: { + videoID: 'vsegshift01', + userID: privateVipUserID, + startTime: 32, + endTime: 42, + } + }, (err, res, body) => { + if (err) return done(err); + if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`); + const expect = [ + { + UUID: 'vsegshifttest01uuid01', + startTime: 0, + endTime: 10, + }, + { + UUID: 'vsegshifttest01uuid02', + startTime: 50, + endTime: 80, + }, + { + UUID: 'vsegshifttest01uuid03', + startTime: 32, + endTime: 35, + }, + { + UUID: 'vsegshifttest01uuid04', + startTime: 110, + endTime: 130, + }, + ]; + done(dbSponsorTimesCompareExpect(db, expect)); + }); + }); + + it('Shift is overlaping endTime of segment', function(done) { + request.post(`${baseURL}/api/segmentShift`, { + json: { + videoID: 'vsegshift01', + userID: privateVipUserID, + startTime: 85, + endTime: 95, + } + }, (err, res, body) => { + if (err) return done(err); + if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`); + const expect = [ + { + UUID: 'vsegshifttest01uuid01', + startTime: 0, + endTime: 10, + }, + { + UUID: 'vsegshifttest01uuid02', + startTime: 60, + endTime: 85, + }, + { + UUID: 'vsegshifttest01uuid03', + startTime: 40, + endTime: 45, + }, + { + UUID: 'vsegshifttest01uuid04', + startTime: 110, + endTime: 130, + }, + ]; + done(dbSponsorTimesCompareExpect(db, expect)); + }); + }); + + it('Shift is overlaping segment', function(done) { + request.post(`${baseURL}/api/segmentShift`, { + json: { + videoID: 'vsegshift01', + userID: privateVipUserID, + startTime: 35, + endTime: 55, + } + }, (err, res, body) => { + if (err) return done(err); + if (res.statusCode !== 200) return done(`Status code was ${res.statusCode}`); + const expect = [ + { + UUID: 'vsegshifttest01uuid01', + startTime: 0, + endTime: 10, + }, + { + UUID: 'vsegshifttest01uuid02', + startTime: 40, + endTime: 70, + }, + { + UUID: 'vsegshifttest01uuid03', + startTime: 40, + endTime: 45, + removed: true, + }, + { + UUID: 'vsegshifttest01uuid04', + startTime: 100, + endTime: 120, + }, + ]; + done(dbSponsorTimesCompareExpect(db, expect)); + }); + }); + + +});