Merge testing & bufgix on logging proxy submissions

This commit is contained in:
Joe Dowd
2020-08-30 20:37:24 +01:00
19 changed files with 527 additions and 214 deletions

View File

@@ -8,6 +8,7 @@
"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]
"userCounterURL": null, // For user counting. URL to instance of https://github.com/ajayyy/PrivacyUserCount
"proxySubmission": null, // Base url to proxy submissions to persist // e.g. https://sponsor.ajay.app (no trailing slash)
"behindProxy": "X-Forwarded-For", //Options: "X-Forwarded-For", "Cloudflare", "X-Real-IP", anything else will mean it is not behind a proxy. True defaults to "X-Forwarded-For"
"db": "./databases/sponsorTimes.db",
@@ -17,5 +18,6 @@
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"mode": "development",
"readOnly": false
"readOnly": false,
"webhooks": []
}

View File

@@ -5,33 +5,20 @@ cd /usr/src/app
cp /etc/sponsorblock/config.json . || cat <<EOF > config.json
{
"port": 8080,
"mysql": {
"host": "127.0.0.1",
"port": 3306,
"database": "sponsorblock",
"user": "sponsorblock",
"password": "sponsorblock"
},
"privateMysql": {
"host": "127.0.0.1",
"port": 3306,
"database": "sponsorblock_private",
"user": "sponsorblock",
"password": "sponsorblock"
},
"globalSalt": "",
"adminUserID": "",
"youtubeAPIKey": "",
"globalSalt": "[CHANGE THIS]",
"adminUserID": "[CHANGE THIS]",
"youtubeAPIKey": null,
"discordReportChannelWebhookURL": null,
"discordFirstTimeSubmissionsWebhookURL": null,
"discordAutoModWebhookURL": null,
"behindProxy": true,
"db": null,
"privateDB": null,
"proxySubmission": null,
"behindProxy": "X-Forwarded-For",
"db": "./databases/sponsorTimes.db",
"privateDB": "./databases/private.db",
"createDatabaseIfNotExist": true,
"schemaFolder": null,
"dbSchema": null,
"privateDBSchema": null,
"schemaFolder": "./databases",
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"mode": "development",
"readOnly": false
}

View File

@@ -6,6 +6,7 @@ var config = require('./config.js');
// Middleware
var corsMiddleware = require('./middleware/cors.js');
var loggerMiddleware = require('./middleware/logger.js');
const userCounter = require('./middleware/userCounter.js');
// Routes
var getSkipSegments = require('./routes/getSkipSegments.js').endpoint;
@@ -33,6 +34,8 @@ app.use(corsMiddleware);
app.use(loggerMiddleware);
app.use(express.json())
if (config.userCounterURL) app.use(userCounter);
// Setup pretty JSON
if (config.mode === "development") app.set('json spaces', 2);

View File

@@ -1,6 +1,5 @@
var MysqlInterface = require('sync-mysql');
var config = require('../config.js');
var logger = require('../utils/logger.js');
const logger = require('../utils/logger.js');
class Mysql {
constructor(msConfig) {

View File

@@ -1,6 +1,6 @@
const log = require('../utils/logger.js'); // log not logger to not interfere with function name
module.exports = function logger (req, res, next) {
log.info('Request recieved: ' + req.url);
log.info("Request recieved: " + req.method + " " + req.url);
next();
}

View File

@@ -0,0 +1,11 @@
var request = require('request');
var config = require('../config.js');
var getIP = require('../utils/getIP.js');
const getHash = require('../utils/getHash.js');
module.exports = function userCounter(req, res, next) {
request.post(config.userCounterURL + "/api/v1/addIP?hashedIP=" + getHash(getIP(req), 1));
next();
}

View File

@@ -20,11 +20,10 @@ function getWeightedRandomChoice(choices, amountOfChoices) {
//assign a weight to each choice
let totalWeight = 0;
choices = choices.map(choice => {
//multiplying by 10 makes around 13 votes the point where it the votes start not mattering as much (10 + 3)
//The 3 makes -2 the minimum votes before being ignored completely
//https://www.desmos.com/calculator/ljftxolg9j
//https://www.desmos.com/calculator/c1duhfrmts
//this can be changed if this system increases in popularity.
const weight = Math.sqrt((choice.votes + 3) * 10);
const weight = Math.exp((choice.votes + 3), 0.85);
totalWeight += weight;
return { ...choice, weight };

View File

@@ -42,11 +42,13 @@ module.exports = function getTopUsers (req, res) {
let rows = db.prepare('all', "SELECT COUNT(*) as totalSubmissions, SUM(views) as viewCount," +
"SUM((sponsorTimes.endTime - sponsorTimes.startTime) / 60 * sponsorTimes.views) as minutesSaved, " +
"SUM(votes) as userVotes, " +
additionalFields +
"IFNULL(userNames.userName, sponsorTimes.userID) as userName FROM sponsorTimes LEFT JOIN userNames ON sponsorTimes.userID=userNames.userID " +
"LEFT JOIN privateDB.shadowBannedUsers ON sponsorTimes.userID=privateDB.shadowBannedUsers.userID " +
"WHERE sponsorTimes.votes > -1 AND sponsorTimes.shadowHidden != 1 AND privateDB.shadowBannedUsers.userID IS NULL " +
"GROUP BY IFNULL(userName, sponsorTimes.userID) ORDER BY " + sortBy + " DESC LIMIT 100", []);
"GROUP BY IFNULL(userName, sponsorTimes.userID) HAVING userVotes > 20 " +
"ORDER BY " + sortBy + " DESC LIMIT 100", []);
for (let i = 0; i < rows.length; i++) {
userNames[i] = rows[i].userName;

View File

@@ -1,21 +1,26 @@
var db = require('../databases/databases.js').db;
var request = require('request');
const db = require('../databases/databases.js').db;
const request = require('request');
const config = require('../config.js');
// A cache of the number of chrome web store users
var chromeUsersCache = null;
var firefoxUsersCache = null;
var lastUserCountCheck = 0;
let chromeUsersCache = null;
let firefoxUsersCache = null;
// By the privacy friendly user counter
let apiUsersCache = null;
let lastUserCountCheck = 0;
module.exports = function getTotalStats (req, res) {
let row = db.prepare('get', "SELECT COUNT(DISTINCT userID) as userCount, COUNT(*) as totalSubmissions, " +
"SUM(views) as viewCount, SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE shadowHidden != 1", []);
"SUM(views) as viewCount, SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes WHERE shadowHidden != 1 AND votes >= 0", []);
if (row !== undefined) {
//send this result
res.send({
userCount: row.userCount,
activeUsers: chromeUsersCache + firefoxUsersCache,
apiUsers: apiUsersCache,
viewCount: row.viewCount,
totalSubmissions: row.totalSubmissions,
minutesSaved: row.minutesSaved
@@ -26,28 +31,37 @@ module.exports = function getTotalStats (req, res) {
if (now - lastUserCountCheck > 5000000) {
lastUserCountCheck = now;
// Get total users
request.get("https://addons.mozilla.org/api/v3/addons/addon/sponsorblock/", function (err, firefoxResponse, body) {
try {
firefoxUsersCache = parseInt(JSON.parse(body).average_daily_users);
request.get("https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone", function(err, chromeResponse, body) {
if (body !== undefined) {
try {
chromeUsersCache = parseInt(body.match(/(?<=\<span class=\"e-f-ih\" title=\").*?(?= users\">)/)[0].replace(",", ""));
} catch (error) {
// Re-check later
lastUserCountCheck = 0;
}
} else {
lastUserCountCheck = 0;
}
});
} catch (error) {
// Re-check later
lastUserCountCheck = 0;
}
});
updateExtensionUsers();
}
}
}
function updateExtensionUsers() {
if (config.userCounterURL) {
request.get(config.userCounterURL + "/api/v1/userCount", (err, response, body) => {
apiUsersCache = Math.max(apiUsersCache, JSON.parse(body).userCount);
});
}
request.get("https://addons.mozilla.org/api/v3/addons/addon/sponsorblock/", function (err, firefoxResponse, body) {
try {
firefoxUsersCache = parseInt(JSON.parse(body).average_daily_users);
request.get("https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone", function(err, chromeResponse, body) {
if (body !== undefined) {
try {
chromeUsersCache = parseInt(body.match(/(?<=\<span class=\"e-f-ih\" title=\").*?(?= users\">)/)[0].replace(",", ""));
} catch (error) {
// Re-check later
lastUserCountCheck = 0;
}
} else {
lastUserCountCheck = 0;
}
});
} catch (error) {
// Re-check later
lastUserCountCheck = 0;
}
});
}

View File

@@ -11,59 +11,89 @@ var isoDurations = require('iso8601-duration');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
var isUserTrustworthy = require('../utils/isUserTrustworthy.js')
var isUserTrustworthy = require('../utils/isUserTrustworthy.js');
const { dispatchEvent } = require('../utils/webhookUtils.js');
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) {
function sendWebhookNotification(userID, videoID, UUID, submissionCount, youtubeData, {submissionStart, submissionEnd}, segmentInfo) {
let row = db.prepare('get', "SELECT userName FROM userNames WHERE userID = ?", [userID]);
let userName = row !== undefined ? row.userName : null;
let video = youtubeData.items[0];
let scopeName = "submissions.other";
if (submissionCount <= 1) {
scopeName = "submissions.new";
}
dispatchEvent(scopeName, {
"video": {
"id": videoID,
"title": video.snippet.title,
"thumbnail": video.snippet.thumbnails.maxres ? video.snippet.thumbnails.maxres : null,
"url": "https://www.youtube.com/watch?v=" + videoID
},
"submission": {
"UUID": UUID,
"category": segmentInfo.category,
"startTime": submissionStart,
"endTime": submissionEnd,
"user": {
"UUID": userID,
"username": userName
}
}
});
}
function sendWebhooks(userID, videoID, UUID, segmentInfo) {
if (config.youtubeAPIKey !== null) {
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [userID]);
// If it is a first time submission
if (userSubmissionCountRow.submissionCount <= 1) {
YouTubeAPI.videos.list({
part: "snippet",
id: videoID
}, function (err, data) {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
}
let startTime = parseFloat(segmentInfo.segment[0]);
let endTime = parseFloat(segmentInfo.segment[1]);
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) +
"\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) {
logger.error("Failed to send first time submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending first time submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
YouTubeAPI.videos.list({
part: "snippet",
id: videoID
}, function (err, data) {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
}
let startTime = parseFloat(segmentInfo.segment[0]);
let endTime = parseFloat(segmentInfo.segment[1]);
sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, data, {submissionStart: startTime, submissionEnd: endTime}, segmentInfo);
// If it is a first time submission
// Then send a notification to discord
if (config.discordFirstTimeSubmissionsWebhookURL === null) 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) +
"\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) {
logger.error("Failed to send first time submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending first time submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
}
});
}
}
@@ -118,9 +148,9 @@ function proxySubmission(req) {
request.post(config.proxySubmission + '/api/skipSegments?userID='+req.query.userID+'&videoID='+req.query.videoID, {json: req.body}, (err, result) => {
if (config.mode === 'development') {
if (!err) {
logger.error('Proxy Submission: ' + result.statusCode + ' ('+result.body+')');
logger.debug('Proxy Submission: ' + result.statusCode + ' ('+result.body+')');
} else {
logger.debug("Proxy Submission: Failed to make call");
logger.error("Proxy Submission: Failed to make call");
}
}
});
@@ -179,7 +209,7 @@ module.exports = async function postSkipSegments(req, res) {
let endTime = parseFloat(segments[i].segment[1]);
if (isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime > endTime) {
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime >= endTime) {
//invalid request
res.sendStatus(400);
return;
@@ -206,6 +236,9 @@ module.exports = async function postSkipSegments(req, res) {
}
}
// Will be filled when submitting
let UUIDs = [];
try {
//check if this user is on the vip list
let vipRow = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]);
@@ -281,8 +314,7 @@ module.exports = async function postSkipSegments(req, res) {
return;
}
// Discord notification
sendDiscordNotification(userID, videoID, UUID, segmentInfo);
UUIDs.push(UUID);
}
} catch (err) {
logger.error(err);
@@ -293,4 +325,8 @@ module.exports = async function postSkipSegments(req, res) {
}
res.sendStatus(200);
for (let i = 0; i < segments.length; i++) {
sendWebhooks(userID, videoID, UUIDs[i], segments[i]);
}
}

View File

@@ -12,12 +12,18 @@ module.exports = function setUsername(req, res) {
let adminUserIDInput = req.query.adminUserID;
if (userID == undefined || userName == undefined || userID === "undefined") {
if (userID == undefined || userName == undefined || userID === "undefined" || userName.length > 50) {
//invalid request
res.sendStatus(400);
return;
}
if (userName.includes("discord")) {
// Don't allow
res.sendStatus(200);
return;
}
if (adminUserIDInput != undefined) {
//this is the admin controlling the other users account, don't hash the controling account's ID
adminUserIDInput = getHash(adminUserIDInput);

View File

@@ -8,6 +8,7 @@ var getHash = require('../utils/getHash.js');
module.exports = async function shadowBanUser(req, res) {
let userID = req.query.userID;
let hashedIP = req.query.hashedIP;
let adminUserIDInput = req.query.adminUserID;
let enabled = req.query.enabled;
@@ -20,7 +21,7 @@ module.exports = async function shadowBanUser(req, res) {
//if enabled is false and the old submissions should be made visible again
let unHideOldSubmissions = req.query.unHideOldSubmissions !== "false";
if (adminUserIDInput == undefined || userID == undefined) {
if (adminUserIDInput == undefined || (userID == undefined && hashedIP == undefined)) {
//invalid request
res.sendStatus(400);
return;
@@ -35,27 +36,57 @@ module.exports = async function shadowBanUser(req, res) {
return;
}
//check to see if this user is already shadowbanned
let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
if (userID) {
//check to see if this user is already shadowbanned
let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?", [userID]);
if (enabled && row.userCount == 0) {
//add them to the shadow ban list
if (enabled && row.userCount == 0) {
//add them to the shadow ban list
//add it to the table
privateDB.prepare('run', "INSERT INTO shadowBannedUsers VALUES(?)", [userID]);
//add it to the table
privateDB.prepare('run', "INSERT INTO shadowBannedUsers VALUES(?)", [userID]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE userID = ?", [userID]);
}
} else if (!enabled && row.userCount > 0) {
//remove them from the shadow ban list
privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
}
} else if (!enabled && row.userCount > 0) {
//remove them from the shadow ban list
privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
//find all previous submissions and unhide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE userID = ?", [userID]);
}
//find all previous submissions and unhide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE userID = ?", [userID]);
}
}
} else if (hashedIP) {
//check to see if this user is already shadowbanned
// let row = privateDB.prepare('get', "SELECT count(*) as userCount FROM shadowBannedIPs WHERE hashedIP = ?", [hashedIP]);
// if (enabled && row.userCount == 0) {
if (enabled) {
//add them to the shadow ban list
//add it to the table
// privateDB.prepare('run', "INSERT INTO shadowBannedIPs VALUES(?)", [hashedIP]);
//find all previous submissions and hide them
if (unHideOldSubmissions) {
db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 1 WHERE timeSubmitted IN " +
"(SELECT privateDB.timeSubmitted FROM sponsorTimes LEFT JOIN privateDB.sponsorTimes as privateDB ON sponsorTimes.timeSubmitted=privateDB.timeSubmitted " +
"WHERE privateDB.hashedIP = ?)", [hashedIP]);
}
} else if (!enabled && row.userCount > 0) {
// //remove them from the shadow ban list
// privateDB.prepare('run', "DELETE FROM shadowBannedUsers WHERE userID = ?", [userID]);
// //find all previous submissions and unhide them
// if (unHideOldSubmissions) {
// db.prepare('run', "UPDATE sponsorTimes SET shadowHidden = 0 WHERE userID = ?", [userID]);
// }
}
}
res.sendStatus(200);

View File

@@ -5,6 +5,7 @@ var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
var isUserTrustworthy = require('../utils/isUserTrustworthy.js');
const { getVoteAuthor, getVoteAuthorRaw, dispatchEvent } = require('../utils/webhookUtils.js');
var databases = require('../databases/databases.js');
var db = databases.db;
@@ -13,6 +14,126 @@ var YouTubeAPI = require('../utils/youtubeAPI.js');
var request = require('request');
const logger = require('../utils/logger.js');
const voteTypes = {
normal: 0,
incorrect: 1
}
/**
* @param {Object} voteData
* @param {string} voteData.UUID
* @param {string} voteData.nonAnonUserID
* @param {number} voteData.voteTypeEnum
* @param {boolean} voteData.isVIP
* @param {boolean} voteData.isOwnSubmission
* @param voteData.row
* @param {string} voteData.category
* @param {number} voteData.incrementAmount
* @param {number} voteData.oldIncrementAmount
*/
function sendWebhooks(voteData) {
let submissionInfoRow = db.prepare('get', "SELECT s.videoID, s.userID, s.startTime, s.endTime, s.category, u.userName, " +
"(select count(1) from sponsorTimes where userID = s.userID) count, " +
"(select count(1) from sponsorTimes where userID = s.userID and votes <= -2) disregarded " +
"FROM sponsorTimes s left join userNames u on s.userID = u.userID where s.UUID=?",
[voteData.UUID]);
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [voteData.nonAnonUserID]);
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
let webhookURL = null;
if (voteData.voteTypeEnum === voteTypes.normal) {
webhookURL = config.discordReportChannelWebhookURL;
} else if (voteData.voteTypeEnum === voteTypes.incorrect) {
webhookURL = config.discordCompletelyIncorrectReportWebhookURL;
}
if (config.youtubeAPIKey !== null) {
YouTubeAPI.videos.list({
part: "snippet",
id: submissionInfoRow.videoID
}, function (err, data) {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
}
let isUpvote = voteData.incrementAmount > 0;
// Send custom webhooks
dispatchEvent(isUpvote ? "vote.up" : "vote.down", {
"user": {
"status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission)
},
"video": {
"id": submissionInfoRow.videoID,
"title": data.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID,
"thumbnail": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : ""
},
"submission": {
"UUID": voteData.UUID,
"views": voteData.row.views,
"category": voteData.category,
"startTime": submissionInfoRow.startTime,
"endTime": submissionInfoRow.endTime,
"user": {
"UUID": submissionInfoRow.userID,
"username": submissionInfoRow.userName,
"submissions": {
"total": submissionInfoRow.count,
"ignored": submissionInfoRow.disregarded
}
}
},
"votes": {
"before": voteData.row.votes,
"after": (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount)
}
});
// Send discord message
if (webhookURL !== null && !isUpvote) {
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": "**" + voteData.row.votes + " Votes Prior | " +
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
+ " Views**\n\n**Submission ID:** " + voteData.UUID
+ "\n**Category:** " + submissionInfoRow.category
+ "\n\n**Submitted by:** "+submissionInfoRow.userName+"\n " + submissionInfoRow.userID
+ "\n\n**Total User Submissions:** "+submissionInfoRow.count
+ "\n**Ignored User Submissions:** "+submissionInfoRow.disregarded
+"\n\n**Timestamp:** " +
getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime),
"color": 10813440,
"author": {
"name": getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission)
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
logger.error("Failed to send reported submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending reported submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
}
});
}
}
}
function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
// Check if they've already made a vote
let previousVoteInfo = privateDB.prepare('get', "select count(*) as votes, category from categoryVotes where UUID = ? and userID = ?", [UUID, userID]);
@@ -97,11 +218,14 @@ async function voteOnSponsorTime(req, res) {
//check if this user is on the vip list
let isVIP = db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [nonAnonUserID]).userCount > 0;
//check if user voting on own submission
let isOwnSubmission = db.prepare("get", "SELECT UUID as submissionCount FROM sponsorTimes where userID = ? AND UUID = ?", [nonAnonUserID, UUID]) !== undefined;
if (type === undefined && category !== undefined) {
return categoryVote(UUID, userID, isVIP, category, hashedIP, res);
}
if (type == 1 && !isVIP) {
if (type == 1 && !isVIP && !isOwnSubmission) {
// Check if upvoting hidden segment
let voteInfo = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", [UUID]);
@@ -111,11 +235,6 @@ async function voteOnSponsorTime(req, res) {
}
}
let voteTypes = {
normal: 0,
incorrect: 1
}
let voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect;
try {
@@ -166,87 +285,19 @@ async function voteOnSponsorTime(req, res) {
let row = db.prepare('get', "SELECT votes, views FROM sponsorTimes WHERE UUID = ?", [UUID]);
if (voteTypeEnum === voteTypes.normal) {
if (isVIP && incrementAmount < 0) {
if ((isVIP || isOwnSubmission) && incrementAmount < 0) {
//this user is a vip and a downvote
incrementAmount = - (row.votes + 2 - oldIncrementAmount);
type = incrementAmount;
}
} else if (voteTypeEnum == voteTypes.incorrect) {
if (isVIP) {
if (isVIP || isOwnSubmission) {
//this user is a vip and a downvote
incrementAmount = 500 * incrementAmount;
type = incrementAmount < 0 ? 12 : 13;
}
}
// Send discord message
if (incrementAmount < 0) {
// Get video ID
let submissionInfoRow = db.prepare('get', "SELECT s.videoID, s.userID, s.startTime, s.endTime, s.category, u.userName, " +
"(select count(1) from sponsorTimes where userID = s.userID) count, " +
"(select count(1) from sponsorTimes where userID = s.userID and votes <= -2) disregarded " +
"FROM sponsorTimes s left join userNames u on s.userID = u.userID where s.UUID=?",
[UUID]);
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [nonAnonUserID]);
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
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 && logger.error(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\n**Submission ID:** " + UUID
+ "\n**Category:** " + submissionInfoRow.category
+ "\n\n**Submitted by:** "+submissionInfoRow.userName+"\n " + submissionInfoRow.userID
+ "\n\n**Total User Submissions:** "+submissionInfoRow.count
+ "\n**Ignored User Submissions:** "+submissionInfoRow.disregarded
+"\n\n**Timestamp:** " +
getFormattedTime(submissionInfoRow.startTime) + " to " + getFormattedTime(submissionInfoRow.endTime),
"color": 10813440,
"author": {
"name": userSubmissionCountRow.submissionCount === 0 ? "Report by New User" : (isVIP ? "Report by VIP User" : "")
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
logger.error("Failed to send reported submission Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending reported submission Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
});
}
}
}
// Only change the database if they have made a submission before and haven't voted recently
let ableToVote = isVIP
|| (db.prepare("get", "SELECT userID FROM sponsorTimes WHERE userID = ?", [nonAnonUserID]) !== undefined
@@ -299,6 +350,18 @@ async function voteOnSponsorTime(req, res) {
}
res.sendStatus(200);
sendWebhooks({
UUID,
nonAnonUserID,
voteTypeEnum,
isVIP,
isOwnSubmission,
row,
category,
incrementAmount,
oldIncrementAmount
});
} catch (err) {
logger.error(err);
@@ -311,4 +374,4 @@ module.exports = {
endpoint: function (req, res) {
voteOnSponsorTime(req, res);
},
};
};

View File

@@ -1,8 +1,9 @@
//converts time in seconds to minutes:seconds
module.exports = function getFormattedTime(seconds) {
let minutes = Math.floor(seconds / 60);
let secondsDisplay = Math.round(seconds - minutes * 60);
if (secondsDisplay < 10) {
module.exports = function getFormattedTime(totalSeconds) {
let minutes = Math.floor(totalSeconds / 60);
let seconds = totalSeconds - minutes * 60;
let secondsDisplay = seconds.toFixed(3);
if (seconds < 10) {
//add a zero
secondsDisplay = "0" + secondsDisplay;
}

View File

@@ -7,6 +7,34 @@ const levels = {
DEBUG: "DEBUG"
};
const colors = {
Reset: "\x1b[0m",
Bright: "\x1b[1m",
Dim: "\x1b[2m",
Underscore: "\x1b[4m",
Blink: "\x1b[5m",
Reverse: "\x1b[7m",
Hidden: "\x1b[8m",
FgBlack: "\x1b[30m",
FgRed: "\x1b[31m",
FgGreen: "\x1b[32m",
FgYellow: "\x1b[33m",
FgBlue: "\x1b[34m",
FgMagenta: "\x1b[35m",
FgCyan: "\x1b[36m",
FgWhite: "\x1b[37m",
BgBlack: "\x1b[40m",
BgRed: "\x1b[41m",
BgGreen: "\x1b[42m",
BgYellow: "\x1b[43m",
BgBlue: "\x1b[44m",
BgMagenta: "\x1b[45m",
BgCyan: "\x1b[46m",
BgWhite: "\x1b[47m",
}
const settings = {
ERROR: true,
WARN: true,
@@ -18,13 +46,17 @@ if (config.mode === 'development') {
settings.INFO = true;
settings.DEBUG = true;
} else if (config.mode === 'test') {
settings.DEBUG = true;
settings.WARN = false;
}
function log(level, string) {
if (!!settings[level]) {
let color = colors.Bright;
if (level === levels.ERROR) color = colors.FgRed;
if (level === levels.WARN) color = colors.FgYellow;
if (level.length === 4) {level = level + " "}; // ensure logs are aligned
console.log(level + " " + new Date().toISOString() + " : " + string);
console.log(colors.Dim, level + " " + new Date().toISOString() + ": ", color, string, colors.Reset);
}
}

52
src/utils/webhookUtils.js Normal file
View File

@@ -0,0 +1,52 @@
const config = require('../config.js');
const logger = require('../utils/logger.js');
const request = require('request');
function getVoteAuthorRaw(submissionCount, isVIP, isOwnSubmission) {
if (isOwnSubmission) {
return "self";
} else if (isVIP) {
return "vip";
} else if (submissionCount === 0) {
return "new";
} else {
return "other";
};
};
function getVoteAuthor(submissionCount, isVIP, isOwnSubmission) {
if (submissionCount === 0) {
return "Report by New User";
} else if (isVIP) {
return "Report by VIP User";
} else if (isOwnSubmission) {
return "Report by Submitter";
}
return "";
}
function dispatchEvent(scope, data) {
let webhooks = config.webhooks;
if (webhooks === undefined || webhooks.length === 0) return;
logger.debug("Dispatching webhooks");
webhooks.forEach(webhook => {
let webhookURL = webhook.url;
let authKey = webhook.key;
let scopes = webhook.scopes || [];
if (!scopes.includes(scope.toLowerCase())) return;
request.post(webhookURL, {json: data, headers: {
"Authorization": authKey,
"Event-Type": scope // Maybe change this in the future?
}}).on('error', (e) => {
logger.warn('Couldn\'t send webhook to ' + webhook.url);
logger.warn(e);
});
});
}
module.exports = {
getVoteAuthorRaw,
getVoteAuthor,
dispatchEvent
}

View File

@@ -15,5 +15,36 @@
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"mode": "test",
"readOnly": false
"readOnly": false,
"webhooks": [
{
"url": "http://127.0.0.1:8081/CustomWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}, {
"url": "http://127.0.0.1:8081/FailedWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}, {
"url": "http://127.0.0.1:8099/WrongPort",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}, {
"url": "http://unresolvable.host:8081/FailedWebhook",
"key": "superSecretKey",
"scopes": [
"vote.up",
"vote.down"
]
}
]
}

View File

@@ -20,6 +20,8 @@ describe('voteOnSponsorTime', () => {
db.exec(startOfQuery + "('voter-submitter2', 1, 11, 2, 'vote-uuid-9', '" + getHash("randomID2") + "', 0, 50, 'sponsor', 0)");
db.exec(startOfQuery + "('voter-submitter2', 1, 11, 2, 'vote-uuid-10', '" + getHash("randomID3") + "', 0, 50, 'sponsor', 0)");
db.exec(startOfQuery + "('voter-submitter2', 1, 11, 2, 'vote-uuid-11', '" + getHash("randomID4") + "', 0, 50, 'sponsor', 0)");
db.exec(startOfQuery + "('own-submission-video', 1, 11, 500, 'own-submission-uuid', '"+ getHash('own-submission-id') +"', 0, 50, 'sponsor', 0)");
db.exec(startOfQuery + "('not-own-submission-video', 1, 11, 500, 'not-own-submission-uuid', '"+ getHash('somebody-else-id') +"', 0, 50, 'sponsor', 0)");
db.exec("INSERT INTO vipUsers (userID) VALUES ('" + getHash("VIPUser") + "')");
privateDB.exec("INSERT INTO shadowBannedUsers (userID) VALUES ('" + getHash("randomID4") + "')");
@@ -151,6 +153,42 @@ describe('voteOnSponsorTime', () => {
});
});
it('should be able to completely downvote your own segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=own-submission-id&UUID=own-submission-uuid&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["own-submission-uuid"]);
if (row.votes <= -2) {
done()
} else {
done("Vote did not succeed. Submission went from 500 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('should not be able to completely downvote somebody elses segment', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=not-own-submission-uuid&type=0", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare('get', "SELECT votes FROM sponsorTimes WHERE UUID = ?", ["not-own-submission-uuid"]);
if (row.votes === 499) {
done()
} else {
done("Vote did not succeed. Submission went from 500 votes to " + row.votes);
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to vote for a category and it should immediately change (for now)', (done) => {
request.get(utils.getbaseURL()
+ "/api/voteOnSponsorTime?userID=randomID2&UUID=vote-uuid-4&category=intro", null,

View File

@@ -15,6 +15,12 @@ app.post('/CompletelyIncorrectReportWebhook', (req, res) => {
res.sendStatus(200);
});
app.post('/CustomWebhook', (req, res) => {
res.sendStatus(200);
});
module.exports = function createMockServer(callback) {
return app.listen(config.mockPort, callback);
}