Added schema upgrade system and started on new vote type

This commit is contained in:
Ajay Ramachandran 2020-04-27 23:01:51 -04:00
parent 74169e6caf
commit d767f2ff6b
9 changed files with 216 additions and 172 deletions

View file

@ -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",

View file

@ -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;

25
databases/_upgrade_0.sql Normal file
View file

@ -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;

View file

@ -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;");

View file

@ -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);
let voteTypes = {
normal: 0,
incorrect: 1
}
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
let incrementAmount = 0;
let oldIncrementAmount = 0;
let voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect;
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;
}
}
try {
//check if vote has already happened
let votesRow = privateDB.prepare("SELECT type FROM votes WHERE userID = ? AND UUID = ?").get(userID, UUID);
//check if this user is on the vip list
let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(nonAnonUserID);
//-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 (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;
}
}
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;
}
//check if this user is on the vip list
let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(nonAnonUserID);
// 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 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);
let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(nonAnonUserID);
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;
}
// Send discord message
if (incrementAmount < 0) {
// Get video ID
let submissionInfoRow = db.prepare("SELECT videoID, userID, startTime, endTime FROM sponsorTimes WHERE UUID = ?").get(UUID);
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 : "",
}
}]
}
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
//check if any submissions are hidden
let hiddenSubmissionsRow = db.prepare("SELECT count(*) as hiddenSubmissions FROM sponsorTimes WHERE userID = ? AND shadowHidden > 0").get(submissionUserID);
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)
}
}
}
if (hiddenSubmissionsRow.hiddenSubmissions > 0) {
//see if some of this users submissions should be visible again
//added to db
res.sendStatus(200);
} catch (err) {
console.error(err);
}
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);
}
}

View file

@ -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
}

View file

@ -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;

View file

@ -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;

View file

@ -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);
}