# Conflicts:
#	src/routes/postSkipSegments.js
#	test/cases/voteOnSponsorTime.js
This commit is contained in:
Ajay Ramachandran
2020-09-15 11:56:14 -04:00
9 changed files with 307 additions and 106 deletions

View File

@@ -1,17 +1,18 @@
var config = require('../config.js');
const config = require('../config.js');
var databases = require('../databases/databases.js');
var db = databases.db;
var privateDB = databases.privateDB;
var YouTubeAPI = require('../utils/youtubeAPI.js');
var logger = require('../utils/logger.js');
var request = require('request');
var isoDurations = require('iso8601-duration');
const databases = require('../databases/databases.js');
const db = databases.db;
const privateDB = databases.privateDB;
const YouTubeAPI = require('../utils/youtubeAPI.js');
const logger = require('../utils/logger.js');
const request = require('request');
const isoDurations = require('iso8601-duration');
const fetch = require('node-fetch');
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 getHash = require('../utils/getHash.js');
const getIP = require('../utils/getIP.js');
const getFormattedTime = require('../utils/getFormattedTime.js');
const isUserTrustworthy = require('../utils/isUserTrustworthy.js')
const { dispatchEvent } = require('../utils/webhookUtils.js');
function sendWebhookNotification(userID, videoID, UUID, submissionCount, youtubeData, {submissionStart, submissionEnd}, segmentInfo) {
@@ -53,11 +54,11 @@ function sendWebhooks(userID, videoID, UUID, segmentInfo) {
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 || userSubmissionCountRow.submissionCount > 1) return;
@@ -67,7 +68,7 @@ function sendWebhooks(userID, videoID, UUID, segmentInfo) {
"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: " +
"\n\nTimestamp: " +
getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n\nCategory: " + segmentInfo.category,
"color": 10813440,
@@ -79,7 +80,7 @@ function sendWebhooks(userID, videoID, UUID, segmentInfo) {
}
}]
}
}, (err, res) => {
}, (err, res) => {
if (err) {
logger.error("Failed to send first time submission Discord hook.");
logger.error(JSON.stringify(err));
@@ -94,35 +95,124 @@ function sendWebhooks(userID, videoID, UUID, segmentInfo) {
}
}
// submission: {videoID, startTime, endTime}
function sendWebhooksNB(userID, videoID, UUID, startTime, endTime, category, probability, ytData) {
let submissionInfoRow = db.prepare('get', "SELECT " +
"(select count(1) from sponsorTimes where userID = ?) count, " +
"(select count(1) from sponsorTimes where userID = ? and votes <= -2) disregarded, " +
"coalesce((select userName FROM userNames WHERE userID = ?), ?) userName",
[userID, userID, userID, userID]);
let submittedBy = "";
// If a userName was created then show both
if (submissionInfoRow.userName !== userID){
submittedBy = submissionInfoRow.userName + "\n " + userID;
} else {
submittedBy = userID;
}
// Send discord message
if (config.discordNeuralBlockRejectWebhookURL === null) return;
request.post(config.discordNeuralBlockRejectWebhookURL, {
json: {
"embeds": [{
"title": ytData.items[0].snippet.title,
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (startTime.toFixed(0) - 2),
"description": "**Submission ID:** " + UUID +
"\n**Timestamp:** " + getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
"\n**Predicted Probability:** " + probability +
"\n**Category:** " + category +
"\n**Submitted by:** "+ submittedBy +
"\n**Total User Submissions:** "+submissionInfoRow.count +
"\n**Ignored User Submissions:** "+submissionInfoRow.disregarded,
"color": 10813440,
"thumbnail": {
"url": ytData.items[0].snippet.thumbnails.maxres ? ytData.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
logger.error("Failed to send NeuralBlock Discord hook.");
logger.error(JSON.stringify(err));
logger.error("\n");
} else if (res && res.statusCode >= 400) {
logger.error("Error sending NeuralBlock Discord hook");
logger.error(JSON.stringify(res));
logger.error("\n");
}
});
}
// callback: function(reject: "String containing reason the submission was rejected")
// returns: string when an error, false otherwise
// Looks like this was broken for no defined youtube key - fixed but IMO we shouldn't return
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(submission, callback) {
// Looks like this was broken for no defined youtube key - fixed but IMO we shouldn't return
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(submission) {
// Get the video information from the youtube API
if (config.youtubeAPIKey !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.listVideos(submission.videoID, "contentDetails", (err, data) => resolve({err, data}));
YouTubeAPI.listVideos(submission.videoID, "contentDetails,snippet", (err, data) => resolve({err, data}));
});
if (err) {
return "Couldn't get video information.";
return false;
} else {
// Check to see if video exists
if (data.pageInfo.totalResults === 0) {
return "No video exists with id " + submission.videoID;
} else {
let duration = data.items[0].contentDetails.duration;
duration = isoDurations.toSeconds(isoDurations.parse(duration));
if (duration == 0) {
// Allow submission if the duration is 0 (bug in youtube api)
return false;
} else if ((submission.endTime - submission.startTime) > (duration/100)*80) {
// Reject submission if over 80% of the video
return "Sponsor segment is over 80% of the video.";
let segments = submission.segments;
let nbString = "";
for (let i = 0; i < segments.length; i++) {
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
let duration = data.items[0].contentDetails.duration;
duration = isoDurations.toSeconds(isoDurations.parse(duration));
if (duration == 0) {
// Allow submission if the duration is 0 (bug in youtube api)
return false;
} else if ((endTime - startTime) > (duration/100)*80) {
// Reject submission if over 80% of the video
return "One of your submitted segments is over 80% of the video.";
} else {
if (segments[i].category === "sponsor") {
//Prepare timestamps to send to NB all at once
nbString = nbString + segments[i].segment[0] + "," + segments[i].segment[1] + ";";
}
}
}
// Check NeuralBlock
let neuralBlockURL = config.neuralBlockURL;
if (!neuralBlockURL) return false;
let response = await fetch(neuralBlockURL + "/api/checkSponsorSegments?vid=" + submission.videoID +
"&segments=" + nbString.substring(0,nbString.length-1));
if (!response.ok) return false;
let nbPredictions = await response.json();
nbDecision = false;
let predictionIdx = 0; //Keep track because only sponsor categories were submitted
for (let i = 0; i < segments.length; i++){
if (segments[i].category === "sponsor"){
if (nbPredictions.probabilities[predictionIdx] < 0.70){
nbDecision = true; // At least one bad entry
startTime = parseFloat(segments[i].segment[0]);
endTime = parseFloat(segments[i].segment[1]);
let UUID = getHash("v2-categories" + submission.videoID + startTime +
endTime + segments[i].category + submission.userID, 1);
// Send to Discord
// Note, if this is too spammy. Consider sending all the segments as one Webhook
sendWebhooksNB(submission.userID, submission.videoID, UUID, startTime, endTime, segments[i].category, nbPredictions.probabilities[predictionIdx], data);
}
predictionIdx++;
}
}
if (nbDecision){
return "Rejected based on NeuralBlock predictions.";
} else {
return false;
}
@@ -170,7 +260,7 @@ module.exports = async function postSkipSegments(req, res) {
//check if all correct inputs are here and the length is 1 second or more
if (videoID == undefined || userID == undefined || segments == undefined || segments.length < 1) {
//invalid request
res.sendStatus(400);
res.status(400).send("Parameters are not valid");
return;
}
@@ -181,13 +271,24 @@ module.exports = async function postSkipSegments(req, res) {
let hashedIP = getHash(getIP(req) + config.globalSalt);
let noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]).map((list) => { return list.category });
//check if this user is on the vip list
let isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
let decreaseVotes = 0;
// Check if all submissions are correct
for (let i = 0; i < segments.length; i++) {
if (segments[i] === undefined || segments[i].segment === undefined || segments[i].category === undefined) {
//invalid request
res.sendStatus(400);
res.status(400).send("One of your segments are invalid");
return;
}
if (!config.categoryList.includes(segments[i].category)) {
res.status("400").send("Category doesn't exist.");
return;
}
// Reject segemnt if it's in the no segments list
if (noSegmentList.indexOf(segments[i].category) !== -1) {
@@ -207,7 +308,7 @@ module.exports = async function postSkipSegments(req, res) {
if (isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime >= endTime) {
//invalid request
res.sendStatus(400);
res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)");
return;
}
@@ -224,21 +325,27 @@ module.exports = async function postSkipSegments(req, res) {
res.sendStatus(409);
return;
}
}
let autoModerateResult = await autoModerateSubmission({videoID, startTime, endTime});
if (autoModerateResult) {
res.status(403).send("Request rejected by auto moderator: " + autoModerateResult);
// Auto moderator check
if (!isVIP) {
let autoModerateResult = await autoModerateSubmission({userID, videoID, segments});//startTime, endTime, category: segments[i].category});
if (autoModerateResult == "Rejected based on NeuralBlock predictions."){
// If NB automod rejects, the submission will start with -2 votes.
// Note, if one submission is bad all submissions will be affected.
// However, this behavior is consistent with other automod functions
// already in place.
//decreaseVotes = -2; //Disable for now
} else if (autoModerateResult) {
//Normal automod behavior
res.status(403).send("Request rejected by auto moderator: " + autoModerateResult + " If this is an issue, send a message on Discord.");
return;
}
}
// 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]);
//get current time
let timeSubmitted = Date.now();
@@ -248,7 +355,7 @@ module.exports = async function postSkipSegments(req, res) {
if (false) {
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?", [hashedIP, videoID, yesterday]);
if (rateLimitCheckRow.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
@@ -261,7 +368,7 @@ module.exports = async function postSkipSegments(req, res) {
if (false) {
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare('get', "SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?", [userID, videoID]);
if (duplicateCheckRow.count >= 16) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
@@ -280,8 +387,8 @@ module.exports = async function postSkipSegments(req, res) {
shadowBanned = 1;
}
let startingVotes = 0;
if (vipRow.userCount > 0) {
let startingVotes = 0 + decreaseVotes;
if (isVIP) {
//this user is a vip, start them at a higher approval rating
startingVotes = 10000;
}
@@ -290,7 +397,7 @@ module.exports = async function postSkipSegments(req, res) {
//this can just be a hash of the data
//it's better than generating an actual UUID like what was used before
//also better for duplication checking
let UUID = getHash("v2-categories" + videoID + segmentInfo.segment[0] +
let UUID = getHash("v2-categories" + videoID + segmentInfo.segment[0] +
segmentInfo.segment[1] + segmentInfo.category + userID, 1);
try {
@@ -306,12 +413,12 @@ module.exports = async function postSkipSegments(req, res) {
} catch (err) {
//a DB change probably occurred
res.sendStatus(502);
logger.error("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
logger.error("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
segmentInfo.segment[1] + ", " + userID + ", " + segmentInfo.category + ". " + err);
return;
}
UUIDs.push(UUID);
}
} catch (err) {

View File

@@ -146,6 +146,11 @@ function categoryVote(UUID, userID, isVIP, category, hashedIP, res) {
res.status("400").send("Submission doesn't exist.");
return;
}
if (!config.categoryList.includes(category)) {
res.status("400").send("Category doesn't exist.");
return;
}
let timeSubmitted = Date.now();