diff --git a/config.json.example b/config.json.example index b8a9d79..ccda7e2 100644 --- a/config.json.example +++ b/config.json.example @@ -7,10 +7,12 @@ "youtubeAPIKey": null, //get this from Google Cloud Platform [optional] "discordReportChannelWebhookURL": null, //URL from discord if you would like notifications when someone makes a report [optional] "discordFirstTimeSubmissionsWebhookURL": null, //URL from discord if you would like notifications when someone makes a first time submission [optional] + "discordCompletelyIncorrectReportWebhookURL": null, //URL from discord if you would like notifications when someone reports a submission as completely incorrect [optional] "behindProxy": true, "db": "./databases/sponsorTimes.db", "privateDB": "./databases/private.db", "createDatabaseIfNotExist": true, //This will run on startup every time (unless readOnly is true) - so ensure "create table if not exists" is used in the schema + "schemaFolder": "./databases", "dbSchema": "./databases/_sponsorTimes.db.sql", "privateDBSchema": "./databases/_private.db.sql", "mode": "development", diff --git a/databases/_sponsorTimes.db.sql b/databases/_sponsorTimes.db.sql index dfca8bf..1fd0f29 100644 --- a/databases/_sponsorTimes.db.sql +++ b/databases/_sponsorTimes.db.sql @@ -18,6 +18,11 @@ CREATE TABLE IF NOT EXISTS "userNames" ( "userID" TEXT NOT NULL, "userName" TEXT NOT NULL ); + +CREATE TABLE IF NOT EXISTS "version" ( + "code" INTEGER NOT NULL default '0' +); + CREATE INDEX IF NOT EXISTS sponsorTimes_videoID on sponsorTimes(videoID); CREATE INDEX IF NOT EXISTS sponsorTimes_UUID on sponsorTimes(UUID); COMMIT; \ No newline at end of file diff --git a/databases/_upgrade_0.sql b/databases/_upgrade_0.sql new file mode 100644 index 0000000..4f747bc --- /dev/null +++ b/databases/_upgrade_0.sql @@ -0,0 +1,25 @@ +BEGIN TRANSACTION; + +/* Add incorrectVotes field */ +CREATE TABLE "sqlb_temp_table_1" ( + "videoID" TEXT NOT NULL, + "startTime" REAL NOT NULL, + "endTime" REAL NOT NULL, + "votes" INTEGER NOT NULL, + "incorrectVotes" INTEGER NOT NULL default '1', + "UUID" TEXT NOT NULL UNIQUE, + "userID" TEXT NOT NULL, + "timeSubmitted" INTEGER NOT NULL, + "views" INTEGER NOT NULL, + "category" TEXT NOT NULL DEFAULT "sponsor", + "shadowHidden" INTEGER NOT NULL +); +INSERT INTO sqlb_temp_table_1 SELECT videoID,startTime,endTime,votes,"1",UUID,userID,timeSubmitted,views,category,shadowHidden FROM sponsorTimes; + +DROP TABLE sponsorTimes; +ALTER TABLE sqlb_temp_table_1 RENAME TO "sponsorTimes"; + +/* Increase version number */ +INSERT INTO version VALUES(1); + +COMMIT; \ No newline at end of file diff --git a/src/databases/databases.js b/src/databases/databases.js index d01d01f..8b4b407 100644 --- a/src/databases/databases.js +++ b/src/databases/databases.js @@ -15,6 +15,13 @@ if (config.createDatabaseIfNotExist && !config.readOnly) { if (fs.existsSync(config.privateDBSchema)) privateDB.exec(fs.readFileSync(config.privateDBSchema).toString()); } +// Upgrade database if required +let versionCode = db.prepare("SELECT code FROM version").get() || 0; +let path = config.schemaFolder + "/_upgrade_" + versionCode + ".sql"; +if (fs.existsSync(path)) { + db.exec(fs.readFileSync(path).toString()); +} + // Enable WAL mode checkpoint number if (!config.readOnly && config.mode === "production") { db.exec("PRAGMA journal_mode=WAL;"); diff --git a/src/routes/voteOnSponsorTime.js b/src/routes/voteOnSponsorTime.js index bc9a326..32ed465 100644 --- a/src/routes/voteOnSponsorTime.js +++ b/src/routes/voteOnSponsorTime.js @@ -11,114 +11,147 @@ var privateDB = databases.privateDB; var YouTubeAPI = require('../utils/youtubeAPI.js'); var request = require('request'); +function completelyIncorrectVote(req, res, params) { + + +} + module.exports = async function voteOnSponsorTime(req, res) { - let UUID = req.query.UUID; - let userID = req.query.userID; - let type = req.query.type; + let UUID = req.query.UUID; + let userID = req.query.userID; + let type = req.query.type; - if (UUID == undefined || userID == undefined || type == undefined) { - //invalid request - res.sendStatus(400); - return; - } + if (UUID == undefined || userID == undefined || type == undefined) { + //invalid request + res.sendStatus(400); + return; + } - //hash the userID - let nonAnonUserID = getHash(userID); - userID = getHash(userID + UUID); + //hash the userID + let nonAnonUserID = getHash(userID); + userID = getHash(userID + UUID); - //x-forwarded-for if this server is behind a proxy - let ip = getIP(req); + //x-forwarded-for if this server is behind a proxy + let ip = getIP(req); - //hash the ip 5000 times so no one can get it from the database - let hashedIP = getHash(ip + config.globalSalt); + //hash the ip 5000 times so no one can get it from the database + let hashedIP = getHash(ip + config.globalSalt); - try { - //check if vote has already happened - let votesRow = privateDB.prepare("SELECT type FROM votes WHERE userID = ? AND UUID = ?").get(userID, UUID); - - //-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future - let incrementAmount = 0; - let oldIncrementAmount = 0; + let voteTypes = { + normal: 0, + incorrect: 1 + } - if (type == 1) { - //upvote - incrementAmount = 1; - } else if (type == 0) { - //downvote - incrementAmount = -1; - } else { - //unrecongnised type of vote - res.sendStatus(400); - return; - } - if (votesRow != undefined) { - if (votesRow.type == 1) { - //upvote - oldIncrementAmount = 1; - } else if (votesRow.type == 0) { - //downvote - oldIncrementAmount = -1; - } else if (votesRow.type == 2) { - //extra downvote - oldIncrementAmount = -4; - } else if (votesRow.type < 0) { - //vip downvote - oldIncrementAmount = votesRow.type; - } - } + let voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect; - //check if this user is on the vip list - let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(nonAnonUserID); + try { + //check if vote has already happened + let votesRow = privateDB.prepare("SELECT type FROM votes WHERE userID = ? AND UUID = ?").get(userID, UUID); + + //-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future + let incrementAmount = 0; + let oldIncrementAmount = 0; - //check if the increment amount should be multiplied (downvotes have more power if there have been many views) - let row = db.prepare("SELECT votes, views FROM sponsorTimes WHERE UUID = ?").get(UUID); - - if (vipRow.userCount != 0 && incrementAmount < 0) { - //this user is a vip and a downvote - incrementAmount = - (row.votes + 2 - oldIncrementAmount); - type = incrementAmount; - } else if (row !== undefined && (row.votes > 8 || row.views > 15) && incrementAmount < 0) { - //increase the power of this downvote - incrementAmount = -Math.abs(Math.min(10, row.votes + 2 - oldIncrementAmount)); - type = incrementAmount; - } + if (type == 1 || type == 11) { + //upvote + incrementAmount = 1; + } else if (type == 0 || type == 10) { + //downvote + incrementAmount = -1; + } else { + //unrecongnised type of vote + res.sendStatus(400); + return; + } + if (votesRow != undefined) { + if (votesRow.type == 1 || type == 11) { + //upvote + oldIncrementAmount = 1; + } else if (votesRow.type == 0 || type == 10) { + //downvote + oldIncrementAmount = -1; + } else if (votesRow.type == 2) { + //extra downvote + oldIncrementAmount = -4; + } else if (votesRow.type < 0) { + //vip downvote + oldIncrementAmount = votesRow.type; + } else if (votesRow.type == 12) { + // VIP downvote for completely incorrect + oldIncrementAmount = -500; + } else if (votesRow.type == 13) { + // VIP upvote for completely incorrect + oldIncrementAmount = 500; + } + } - // Send discord message - if (type != 1) { - // Get video ID - let submissionInfoRow = db.prepare("SELECT videoID, userID, startTime, endTime FROM sponsorTimes WHERE UUID = ?").get(UUID); + //check if this user is on the vip list + let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(nonAnonUserID); - let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(nonAnonUserID); + //check if the increment amount should be multiplied (downvotes have more power if there have been many views) + let row = db.prepare("SELECT votes, views FROM sponsorTimes WHERE UUID = ?").get(UUID); + + if (voteTypeEnum == voteTypes.normal) { + if (vipRow.userCount != 0 && incrementAmount < 0) { + //this user is a vip and a downvote + incrementAmount = - (row.votes + 2 - oldIncrementAmount); + type = incrementAmount; + } else if (row !== undefined && (row.votes > 8 || row.views > 15) && incrementAmount < 0) { + //increase the power of this downvote + incrementAmount = -Math.abs(Math.min(10, row.votes + 2 - oldIncrementAmount)); + type = incrementAmount; + } + } else if (voteTypeEnum == voteTypes.incorrect) { + if (vipRow.userCount != 0) { + //this user is a vip and a downvote + incrementAmount = 500 * incrementAmount; + type = incrementAmount < 0 ? 12 : 13; + } + } - if (config.youtubeAPIKey !== null && config.discordReportChannelWebhookURL !== null) { - YouTubeAPI.videos.list({ - part: "snippet", - id: submissionInfoRow.videoID - }, function (err, data) { - if (err || data.items.length === 0) { - err && console.log(err); - return; - } - - request.post(config.discordReportChannelWebhookURL, { - json: { - "embeds": [{ - "title": data.items[0].snippet.title, - "url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID + - "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2), - "description": "**" + row.votes + " Votes Prior | " + (row.votes + incrementAmount - oldIncrementAmount) + " Votes Now | " + row.views + - " Views**\n\nSubmission ID: " + UUID + - "\n\nSubmitted by: " + submissionInfoRow.userID + "\n\nTimestamp: " + - getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime), - "color": 10813440, - "author": { - "name": userSubmissionCountRow.submissionCount === 0 ? "Report by New User" : (vipRow.userCount !== 0 ? "Report by VIP User" : "") - }, - "thumbnail": { - "url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "", - } - }] - } + // Send discord message + if (incrementAmount < 0) { + // Get video ID + let submissionInfoRow = db.prepare("SELECT videoID, userID, startTime, endTime FROM sponsorTimes WHERE UUID = ?").get(UUID); + + let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(nonAnonUserID); + + let webhookURL = null; + if (voteTypeEnum === voteTypes.normal) { + webhookURL = config.discordReportChannelWebhookURL; + } else if (voteTypeEnum === voteTypes.incorrect) { + webhookURL = config.discordCompletelyIncorrectReportWebhookURL; + } + + if (config.youtubeAPIKey !== null && webhookURL !== null) { + YouTubeAPI.videos.list({ + part: "snippet", + id: submissionInfoRow.videoID + }, function (err, data) { + if (err || data.items.length === 0) { + err && console.log(err); + return; + } + + request.post(webhookURL, { + json: { + "embeds": [{ + "title": data.items[0].snippet.title, + "url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID + + "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2), + "description": "**" + row.votes + " Votes Prior | " + (row.votes + incrementAmount - oldIncrementAmount) + " Votes Now | " + row.views + + " Views**\n\nSubmission ID: " + UUID + + "\n\nSubmitted by: " + submissionInfoRow.userID + "\n\nTimestamp: " + + getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime), + "color": 10813440, + "author": { + "name": userSubmissionCountRow.submissionCount === 0 ? "Report by New User" : (vipRow.userCount !== 0 ? "Report by VIP User" : "") + }, + "thumbnail": { + "url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "", + } + }] + } }, (err, res) => { if (err) { console.log("Failed to send reported submission Discord hook."); @@ -129,43 +162,50 @@ module.exports = async function voteOnSponsorTime(req, res) { console.log(JSON.stringify(res)); console.log("\n"); } - }); - }); - } - } + }); + }); + } + } - //update the votes table - if (votesRow != undefined) { - privateDB.prepare("UPDATE votes SET type = ? WHERE userID = ? AND UUID = ?").run(type, userID, UUID); - } else { - privateDB.prepare("INSERT INTO votes VALUES(?, ?, ?, ?)").run(UUID, userID, hashedIP, type); - } + //update the votes table + if (votesRow != undefined) { + privateDB.prepare("UPDATE votes SET type = ? WHERE userID = ? AND UUID = ?").run(type, userID, UUID); + } else { + privateDB.prepare("INSERT INTO votes VALUES(?, ?, ?, ?)").run(UUID, userID, hashedIP, type); + } - //update the vote count on this sponsorTime - //oldIncrementAmount will be zero is row is null - db.prepare("UPDATE sponsorTimes SET votes = votes + ? WHERE UUID = ?").run(incrementAmount - oldIncrementAmount, UUID); + let tableName = ""; + if (voteTypeEnum === voteTypes.normal) { + tableName = "votes"; + } else if (voteTypeEnum === voteTypes.incorrect) { + tableName = "incorrectVotes"; + } - //for each positive vote, see if a hidden submission can be shown again - if (incrementAmount > 0) { - //find the UUID that submitted the submission that was voted on - let submissionUserID = db.prepare("SELECT userID FROM sponsorTimes WHERE UUID = ?").get(UUID).userID; + //update the vote count on this sponsorTime + //oldIncrementAmount will be zero is row is null + db.prepare("UPDATE sponsorTimes SET " + tableName + " += ? WHERE UUID = ?").run(incrementAmount - oldIncrementAmount, UUID); - //check if any submissions are hidden - let hiddenSubmissionsRow = db.prepare("SELECT count(*) as hiddenSubmissions FROM sponsorTimes WHERE userID = ? AND shadowHidden > 0").get(submissionUserID); + //for each positive vote, see if a hidden submission can be shown again + if (incrementAmount > 0) { + //find the UUID that submitted the submission that was voted on + let submissionUserID = db.prepare("SELECT userID FROM sponsorTimes WHERE UUID = ?").get(UUID).userID; - 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("UPDATE sponsorTimes SET shadowHidden = 0 WHERE ROWID IN (SELECT ROWID FROM sponsorTimes WHERE userID = ? AND shadowHidden = 1 LIMIT 2)").run(submissionUserID) - } - } - } + //check if any submissions are hidden + let hiddenSubmissionsRow = db.prepare("SELECT count(*) as hiddenSubmissions FROM sponsorTimes WHERE userID = ? AND shadowHidden > 0").get(submissionUserID); - //added to db - res.sendStatus(200); - } catch (err) { - console.error(err); - } + 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("UPDATE sponsorTimes SET shadowHidden = 0 WHERE ROWID IN (SELECT ROWID FROM sponsorTimes WHERE userID = ? AND shadowHidden = 1 LIMIT 2)").run(submissionUserID) + } + } + } + + //added to db + res.sendStatus(200); + } catch (err) { + console.error(err); + } } \ No newline at end of file diff --git a/test.json b/test.json index f07fbf8..28373a1 100644 --- a/test.json +++ b/test.json @@ -6,12 +6,14 @@ "youtubeAPIKey": "", "discordReportChannelWebhookURL": "http://127.0.0.1:8081/ReportChannelWebhook", "discordFirstTimeSubmissionsWebhookURL": "http://127.0.0.1:8081/FirstTimeSubmissionsWebhook", + "discordCompletelyIncorrectReportWebhookURL": "http://127.0.0.1:8081/CompletelyIncorrectReportWebhook", "behindProxy": true, "db": "./test/databases/sponsorTimes.db", "privateDB": "./test/databases/private.db", "createDatabaseIfNotExist": true, - "dbSchema": "./test/databases/_sponsorTimes.db.sql", - "privateDBSchema": "./test/databases/_private.db.sql", + "schemaFolder": "./databases", + "dbSchema": "./databases/_sponsorTimes.db.sql", + "privateDBSchema": "./databases/_private.db.sql", "mode": "test", "readOnly": false } diff --git a/test/databases/_private.db.sql b/test/databases/_private.db.sql deleted file mode 100644 index b93aaee..0000000 --- a/test/databases/_private.db.sql +++ /dev/null @@ -1,18 +0,0 @@ -BEGIN TRANSACTION; -CREATE TABLE IF NOT EXISTS "shadowBannedUsers" ( - "userID" TEXT NOT NULL -); -CREATE TABLE IF NOT EXISTS "votes" ( - "UUID" TEXT NOT NULL, - "userID" INTEGER NOT NULL, - "hashedIP" INTEGER NOT NULL, - "type" INTEGER NOT NULL -); -CREATE TABLE IF NOT EXISTS "sponsorTimes" ( - "videoID" TEXT NOT NULL, - "hashedIP" TEXT NOT NULL, - "timeSubmitted" INTEGER NOT NULL -); -CREATE INDEX IF NOT EXISTS sponsorTimes_hashedIP on sponsorTimes(hashedIP); -CREATE INDEX IF NOT EXISTS votes_userID on votes(UUID); -COMMIT; diff --git a/test/databases/_sponsorTimes.db.sql b/test/databases/_sponsorTimes.db.sql deleted file mode 100644 index dfca8bf..0000000 --- a/test/databases/_sponsorTimes.db.sql +++ /dev/null @@ -1,23 +0,0 @@ -BEGIN TRANSACTION; -CREATE TABLE IF NOT EXISTS "vipUsers" ( - "userID" TEXT NOT NULL -); -CREATE TABLE IF NOT EXISTS "sponsorTimes" ( - "videoID" TEXT NOT NULL, - "startTime" REAL NOT NULL, - "endTime" REAL NOT NULL, - "votes" INTEGER NOT NULL, - "UUID" TEXT NOT NULL UNIQUE, - "userID" TEXT NOT NULL, - "timeSubmitted" INTEGER NOT NULL, - "views" INTEGER NOT NULL, - "category" TEXT NOT NULL, - "shadowHidden" INTEGER NOT NULL -); -CREATE TABLE IF NOT EXISTS "userNames" ( - "userID" TEXT NOT NULL, - "userName" TEXT NOT NULL -); -CREATE INDEX IF NOT EXISTS sponsorTimes_videoID on sponsorTimes(videoID); -CREATE INDEX IF NOT EXISTS sponsorTimes_UUID on sponsorTimes(UUID); -COMMIT; \ No newline at end of file diff --git a/test/mocks.js b/test/mocks.js index 49060af..b58a47f 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -11,6 +11,10 @@ app.post('/FirstTimeSubmissionsWebhook', (req, res) => { res.sendStatus(200); }); +app.post('/CompletelyIncorrectReportWebhook', (req, res) => { + res.sendStatus(200); +}); + module.exports = function createMockServer(callback) { return app.listen(config.mockPort, callback); } \ No newline at end of file