Merge pull request #71 from ajayyy/categories

Categories + Github Actions and Tests Fixes
This commit is contained in:
Ajay Ramachandran 2020-04-07 14:50:48 -04:00 committed by GitHub
commit b2147d42a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 978 additions and 369 deletions

18
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: CI
on: [push, pull_request]
jobs:
build:
name: Run Tests
runs-on: ubuntu-latest
steps:
# Initialization
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- run: npm install
- name: Run Tests
run: npm test

View file

@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS "sponsorTimes" (
"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" (

View file

@ -8,8 +8,8 @@ var corsMiddleware = require('./middleware/cors.js');
var loggerMiddleware = require('./middleware/logger.js');
// Routes
var getVideoSponsorTimes = require('./routes/getVideoSponsorTimes.js');
var submitSponsorTimes = require('./routes/submitSponsorTimes.js');
var getSkipSegments = require('./routes/getSkipSegments.js').endpoint;
var postSkipSegments = require('./routes/postSkipSegments.js');
var voteOnSponsorTime = require('./routes/voteOnSponsorTime.js');
var viewedVideoSponsorTime = require('./routes/viewedVideoSponsorTime.js');
var setUsername = require('./routes/setUsername.js');
@ -22,17 +22,29 @@ var getTopUsers = require('./routes/getTopUsers.js');
var getTotalStats = require('./routes/getTotalStats.js');
var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js');
// Old Routes
var oldGetVideoSponsorTimes = require('./routes/oldGetVideoSponsorTimes.js');
var oldSubmitSponsorTimes = require('./routes/oldSubmitSponsorTimes.js');
//setup CORS correctly
app.use(corsMiddleware);
app.use(loggerMiddleware);
app.use(express.json())
// Setup pretty JSON
if (config.mode === "development") app.set('json spaces', 2);
//add the get function
app.get('/api/getVideoSponsorTimes', getVideoSponsorTimes);
app.get('/api/getVideoSponsorTimes', oldGetVideoSponsorTimes);
//add the post function
app.get('/api/postVideoSponsorTimes', submitSponsorTimes);
app.post('/api/postVideoSponsorTimes', submitSponsorTimes);
//add the oldpost function
app.get('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
app.post('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
//add the skip segments functions
app.get('/api/skipSegments', getSkipSegments);
app.post('/api/skipSegments', postSkipSegments);
//voting endpoint
app.get('/api/voteOnSponsorTime', voteOnSponsorTime);

View file

@ -200,71 +200,109 @@ function getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs) {
};
}
/**
*
* Returns what would be sent to the client.
* Will resond with errors if required. Returns false if it errors.
*
* @param req
* @param res
*
* @returns
*/
function handleGetSegments(req, res) {
const videoID = req.body.videoID || req.query.videoID;
// Default to sponsor
// If using params instead of JSON, only one category can be pulled
const categories = req.body.categories || (req.query.category ? [req.query.category] : ["sponsor"]);
/**
* @type {Array<{
* segment: number[],
* category: string,
* UUID: string
* }>
* }
*/
let segments = [];
let hashedIP = getHash(getIP(req) + config.globalSalt);
try {
for (const category of categories) {
let rows = db.prepare("SELECT startTime, endTime, votes, UUID, shadowHidden FROM sponsorTimes WHERE videoID = ? and category = ? ORDER BY startTime")
.all(videoID, category);
let sponsorTimes = [];
let votes = []
let UUIDs = [];
for (let i = 0; i < rows.length; i++) {
//check if votes are above -1
if (rows[i].votes < -1) {
//too untrustworthy, just ignore it
continue;
}
//check if shadowHidden
//this means it is hidden to everyone but the original ip that submitted it
if (rows[i].shadowHidden == 1) {
//get the ip
//await the callback
let hashedIPRow = privateDB.prepare("SELECT hashedIP FROM sponsorTimes WHERE videoID = ?").all(videoID);
if (!hashedIPRow.some((e) => e.hashedIP === hashedIP)) {
//this isn't their ip, don't send it to them
continue;
}
}
sponsorTimes.push([rows[i].startTime, rows[i].endTime]);
votes.push(rows[i].votes);
UUIDs.push(rows[i].UUID);
}
if (sponsorTimes.length == 0) {
res.sendStatus(404);
return false;
}
organisedData = getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs);
sponsorTimes = organisedData.sponsorTimes;
UUIDs = organisedData.UUIDs;
for (let i = 0; i < sponsorTimes.length; i++) {
segments.push({
segment: sponsorTimes[i],
category: category,
UUID: UUIDs[i]
});
}
}
} catch(error) {
console.error(error);
res.send(500);
return false;
}
if (segments.length == 0) {
res.sendStatus(404);
return false;
}
return segments;
}
module.exports = function (req, res) {
let videoID = req.query.videoID;
module.exports = {
handleGetSegments,
endpoint: function (req, res) {
let segments = handleGetSegments(req, res);
let sponsorTimes = [];
let votes = []
let UUIDs = [];
let hashedIP = getHash(getIP(req) + config.globalSalt);
try {
let rows = db.prepare("SELECT startTime, endTime, votes, UUID, shadowHidden FROM sponsorTimes WHERE videoID = ? ORDER BY startTime").all(videoID);
for (let i = 0; i < rows.length; i++) {
//check if votes are above -1
if (rows[i].votes < -1) {
//too untrustworthy, just ignore it
continue;
}
//check if shadowHidden
//this means it is hidden to everyone but the original ip that submitted it
if (rows[i].shadowHidden == 1) {
//get the ip
//await the callback
let hashedIPRow = privateDB.prepare("SELECT hashedIP FROM sponsorTimes WHERE videoID = ?").all(videoID);
if (!hashedIPRow.some((e) => e.hashedIP === hashedIP)) {
//this isn't their ip, don't send it to them
continue;
}
}
sponsorTimes.push([]);
let index = sponsorTimes.length - 1;
sponsorTimes[index][0] = rows[i].startTime;
sponsorTimes[index][1] = rows[i].endTime;
votes[index] = rows[i].votes;
UUIDs[index] = rows[i].UUID;
}
if (sponsorTimes.length == 0) {
res.sendStatus(404);
return;
}
organisedData = getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs);
sponsorTimes = organisedData.sponsorTimes;
UUIDs = organisedData.UUIDs;
if (sponsorTimes.length == 0) {
res.sendStatus(404);
} else {
//send result
res.send({
sponsorTimes: sponsorTimes,
UUIDs: UUIDs
})
}
} catch(error) {
console.error(error);
res.send(500);
}
if (segments) {
//send result
res.send(segments)
}
}
}

View file

@ -0,0 +1,26 @@
var getSkipSegments = require("./getSkipSegments.js")
module.exports = function (req, res) {
let videoID = req.query.videoID;
let segments = getSkipSegments.handleGetSegments(req, res);
if (segments) {
// Convert to old outputs
let sponsorTimes = [];
let UUIDs = [];
for (const segment of segments) {
sponsorTimes.push(segment.segment);
UUIDs.push(segment.UUID);
}
res.send({
sponsorTimes,
UUIDs
})
}
// Error has already been handled in the other method
}

View file

@ -0,0 +1,9 @@
var config = require('../config.js');
var postSkipSegments = require('./postSkipSegments.js');
module.exports = async function submitSponsorTimes(req, res) {
req.query.category = "sponsor";
return postSkipSegments(req, res);
}

View file

@ -0,0 +1,266 @@
var 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 request = require('request');
var isoDurations = require('iso8601-duration');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
// TODO: might need to be a util
//returns true if the user is considered trustworthy
//this happens after a user has made 5 submissions and has less than 60% downvoted submissions
async function isUserTrustworthy(userID) {
//check to see if this user how many submissions this user has submitted
let totalSubmissionsRow = db.prepare("SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?").get(userID);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
let downvotedSubmissionsRow = db.prepare("SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)").get(userID);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}
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) {
let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(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 && console.log(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) {
console.log("Failed to send first time submission Discord hook.");
console.log(JSON.stringify(err));
console.log("\n");
} else if (res && res.statusCode >= 400) {
console.log("Error sending first time submission Discord hook");
console.log(JSON.stringify(res));
console.log("\n");
}
});
});
}
}
}
// submission: {videoID, startTime, endTime}
// callback: function(reject: "String containing reason the submission was rejected")
// returns: string when an error, false otherwise
async function autoModerateSubmission(submission, callback) {
// Get the video information from the youtube API
if (config.youtubeAPI !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.videos.list({
part: "contentDetails",
id: submission.videoID
}, (err, data) => resolve({err, data}));
});
if (err) {
return "Couldn't get video information.";
} else {
// Check to see if video exists
if (data.pageInfo.totalResults === 0) {
callback("No video exists with id " + submission.videoID);
} else {
let duration = data.items[0].contentDetails.duration;
duration = isoDurations.toSeconds(isoDurations.parse(duration));
// Reject submission if over 80% of the video
if ((submission.endTime - submission.startTime) > (duration/100)*80) {
return "Sponsor segment is over 80% of the video.";
} else {
return false;
}
}
}
} else {
console.log("Skipped YouTube API");
// Can't moderate the submission without calling the youtube API
// so allow by default.
return;
}
}
module.exports = async function postSkipSegments(req, res) {
let videoID = req.query.videoID || req.body.videoID;
let userID = req.query.userID || req.body.userID;
let segments = req.body.segments;
if (segments === undefined) {
// Use query instead
segments = [{
segment: [req.query.startTime, req.query.endTime],
category: req.query.category
}];
}
//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);
return;
}
//hash the userID
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
// 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);
return;
}
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
if (Math.abs(startTime - endTime) < 1 || isNaN(startTime) || isNaN(endTime)
|| startTime === Infinity || endTime === Infinity || startTime > endTime) {
//invalid request
res.sendStatus(400);
return;
}
//check if this info has already been submitted before
let duplicateCheck2Row = db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE startTime = ? " +
"and endTime = ? and category = ? and videoID = ?").get(startTime, endTime, segments[i].category, videoID);
if (duplicateCheck2Row.count > 0) {
res.sendStatus(409);
return;
}
let autoModerateResult = await autoModerateSubmission({videoID, startTime, endTime});
if (autoModerateResult) {
res.status(403).send("Request rejected by auto moderator: " + autoModerateResult);
return;
}
}
try {
//check if this user is on the vip list
let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(userID);
//get current time
let timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?").get([hashedIP, videoID, yesterday]);
if (rateLimitCheckRow.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
return;
}
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?").get([userID, videoID]);
if (duplicateCheckRow.count >= 8) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
return;
}
//check to see if this user is shadowbanned
let shadowBanRow = privateDB.prepare("SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?").get(userID);
let shadowBanned = shadowBanRow.userCount;
if (!(await isUserTrustworthy(userID))) {
//hide this submission as this user is untrustworthy
shadowBanned = 1;
}
let startingVotes = 0;
if (vipRow.userCount > 0) {
//this user is a vip, start them at a higher approval rating
startingVotes = 10;
}
for (const segmentInfo of segments) {
//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] +
segmentInfo.segment[1] + segmentInfo.category + userID, 1);
try {
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, segmentInfo.segment[0],
segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned);
//add to private db as well
privateDB.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?)").run(videoID, hashedIP, timeSubmitted);
} catch (err) {
//a DB change probably occurred
res.sendStatus(502);
console.log("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
segmentInfo.segment[1] + ", " + userID + ", " + segmentInfo.category + ". " + err);
return;
}
// Discord notification
sendDiscordNotification(userID, videoID, UUID, segmentInfo);
}
} catch (err) {
console.error(err);
res.sendStatus(500);
return;
}
res.sendStatus(200);
}

View file

@ -1,233 +0,0 @@
var 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 request = require('request');
var isoDurations = require('iso8601-duration');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
// TODO: might need to be a util
//returns true if the user is considered trustworthy
//this happens after a user has made 5 submissions and has less than 60% downvoted submissions
async function isUserTrustworthy(userID) {
//check to see if this user how many submissions this user has submitted
let totalSubmissionsRow = db.prepare("SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?").get(userID);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
let downvotedSubmissionsRow = db.prepare("SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)").get(userID);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}
module.exports = async function submitSponsorTimes(req, res) {
let videoID = req.query.videoID;
let startTime = req.query.startTime;
let endTime = req.query.endTime;
let userID = req.query.userID;
//check if all correct inputs are here and the length is 1 second or more
if (videoID == undefined || startTime == undefined || endTime == undefined || userID == undefined
|| Math.abs(startTime - endTime) < 1) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
startTime = parseFloat(startTime);
endTime = parseFloat(endTime);
if (isNaN(startTime) || isNaN(endTime)) {
//invalid request
res.sendStatus(400);
return;
}
if (startTime === Infinity || endTime === Infinity) {
//invalid request
res.sendStatus(400);
return;
}
if (startTime > endTime) {
//time can't go backwards
res.sendStatus(400);
return;
}
try {
//check if this user is on the vip list
let vipRow = db.prepare("SELECT count(*) as userCount FROM vipUsers WHERE userID = ?").get(userID);
//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(videoID + startTime + endTime + userID, 1);
//get current time
let timeSubmitted = Date.now();
let yesterday = timeSubmitted - 86400000;
//check to see if this ip has submitted too many sponsors today
let rateLimitCheckRow = privateDB.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?").get([hashedIP, videoID, yesterday]);
if (rateLimitCheckRow.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
} else {
//check to see if the user has already submitted sponsors for this video
let duplicateCheckRow = db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?").get([userID, videoID]);
if (duplicateCheckRow.count >= 8) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
} else {
//check if this info has already been submitted first
let duplicateCheck2Row = db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID]);
//check to see if this user is shadowbanned
let shadowBanRow = privateDB.prepare("SELECT count(*) as userCount FROM shadowBannedUsers WHERE userID = ?").get(userID);
let shadowBanned = shadowBanRow.userCount;
if (!(await isUserTrustworthy(userID))) {
//hide this submission as this user is untrustworthy
shadowBanned = 1;
}
let startingVotes = 0;
if (vipRow.userCount > 0) {
//this user is a vip, start them at a higher approval rating
startingVotes = 10;
}
if (duplicateCheck2Row == null) {
//not a duplicate
autoModerateSubmission({videoID, startTime, endTime}, (reject) => {
if (!reject) {
try {
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, startTime, endTime, startingVotes, UUID, userID, timeSubmitted, 0, shadowBanned);
//add to private db as well
privateDB.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?)").run(videoID, hashedIP, timeSubmitted);
res.sendStatus(200);
} catch (err) {
//a DB change probably occurred
console.log("Error when putting sponsorTime in the DB: " + videoID + ", " + startTime + ", " + "endTime" + ", " + userID);
res.sendStatus(502);
}
} else {
res.status(403).send("Request rejected by auto moderator: " + reject);
}
});
} else {
res.sendStatus(409);
}
}
}
} catch (err) {
console.error(err);
res.send(500);
}
}
// submission: {videoID, startTime, endTime}
// callback: function(reject: "String containing reason the submission was rejected")
const autoModerateSubmission = (submission, callback) => {
// Get the video information from the youtube API
if (config.youtubeAPI !== null) {
YouTubeAPI.videos.list({
part: "contentDetails",
id: submission.videoID
}, (err, data) => {
if (err) callback("Couldn't get video information.");
else {
// Check to see if video exists
if (data.pageInfo.totalResults === 0) {
callback("No video exists with id " + submission.videoID);
} else {
let duration = data.items[0].contentDetails.duration;
duration = isoDurations.toSeconds(isoDurations.parse(duration));
// Reject submission if over 80% of the video
if ((submission.endTime - submission.startTime) > (duration/100)*80) {
callback ("Sponsor segment is over 80% of the video.");
} else {
callback();
}
}
}
});
} else {
console.log("skip youtube api");
// Can't moderate the submission without calling the youtube API
// so allow by default.
callback();
}
}
/*
//check if they are a first time user
//if so, send a notification to discord
if (config.youtubeAPIKey !== null && config.discordFirstTimeSubmissionsWebhookURL !== null && duplicateCheck2Row == null) {
let userSubmissionCountRow = db.prepare("SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?").get(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 && console.log(err);
return;
}
console.log(JSON.stringify(data));
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),
"color": 10813440,
"author": {
"name": userID
},
"thumbnail": {
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
}
}]
}
}, (err, res) => {
if (err) {
console.log("Failed to send first time submission Discord hook.");
console.log(JSON.stringify(err));
console.log("\n");
} else if (res && res.statusCode >= 400) {
console.log("Error sending first time submission Discord hook");
console.log(JSON.stringify(res));
console.log("\n");
}
});
});
}
}*/

View file

@ -1,5 +1,5 @@
var fs = require('fs');
var config = JSON.parse(fs.readFileSync('config.json'));
var config = require('../config.js');
module.exports = function getIP(req) {
return config.behindProxy ? req.headers['x-forwarded-for'] : req.connection.remoteAddress;

View file

@ -2,6 +2,11 @@ var Mocha = require('mocha'),
fs = require('fs'),
path = require('path');
var config = require('./src/config.js');
// delete old test database
if (fs.existsSync(config.db)) fs.unlinkSync(config.db);
if (fs.existsSync(config.privateDB)) fs.unlinkSync(config.privateDB);
var createServer = require('./src/app.js');
var createMockServer = require('./test/mocks.js');

View file

@ -5,7 +5,7 @@ var getHash = require('../../src/utils/getHash.js');
describe('getSavedTimeForUser', () => {
before(() => {
db.exec("INSERT INTO sponsorTimes VALUES ('getSavedTimeForUser', 1, 11, 2, 'abc1239999', '"+getHash("testman")+"', 0, 50, 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('getSavedTimeForUser', 1, 11, 2, 'abc1239999', '" + getHash("testman") + "', 0, 50, 'sponsor', 0)");
});
it('Should be able to get a 200', (done) => {

View file

@ -0,0 +1,248 @@
var request = require('request');
var db = require('../../src/databases/databases.js').db;
var utils = require('../utils.js');
/*
*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,
"shadowHidden" INTEGER NOT NULL
);
*/
describe('getSkipSegments', () => {
before(() => {
db.exec("INSERT INTO sponsorTimes VALUES ('testtesttest', 1, 11, 2, '1-uuid-0', 'testman', 0, 50, 'sponsor', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('testtesttest', 20, 33, 2, '1-uuid-2', 'testman', 0, 50, 'intro', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('testtesttest,test', 1, 11, 2, '1-uuid-1', 'testman', 0, 50, 'sponsor', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('test3', 1, 11, 2, '1-uuid-4', 'testman', 0, 50, 'sponsor', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('test3', 7, 22, -3, '1-uuid-5', 'testman', 0, 50, 'sponsor', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('multiple', 1, 11, 2, '1-uuid-6', 'testman', 0, 50, 'intro', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('multiple', 20, 33, 2, '1-uuid-7', 'testman', 0, 50, 'intro', 0)");
});
it('Should be able to get a time by category (Query Method) 1', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-0") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get a time by category (Query Method) 2', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&category=intro", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 20 && data[0].segment[1] === 33
&& data[0].category === "intro" && data[0].UUID === "1-uuid-2") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should be able to get a time by category (JSON Method) 1', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments", {
json: {
videoID: "testtesttest",
categories: ["sponsor"]
}
},
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = res.body;
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-0") {
done();
} else {
done("Received incorrect body: " + JSON.stringify(res.body));
}
}
});
});
it('Should be able to get a time by category (JSON Method) 2', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments", {
json: {
videoID: "testtesttest",
categories: ["intro"]
}
},
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = res.body;
if (data.length === 1 && data[0].segment[0] === 20 && data[0].segment[1] === 33
&& data[0].category === "intro" && data[0].UUID === "1-uuid-2") {
done();
} else {
done("Received incorrect body: " + JSON.stringify(res.body));
}
}
});
});
it('Should be able to get multiple times by category (JSON Method) 1', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments", {
json: {
videoID: "multiple",
categories: ["intro"]
}
},
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = res.body;
if (data.length === 2) {
let success = true;
for (const segment of data) {
if ((segment.segment[0] !== 20 || segment.segment[1] !== 33
|| segment.category !== "intro" || segment.UUID !== "1-uuid-7") &&
(segment.segment[0] !== 1 || segment.segment[1] !== 11
|| segment.category !== "intro" || segment.UUID !== "1-uuid-6")) {
success = false;
break;
}
}
if (success) done();
else done("Received incorrect body: " + JSON.stringify(res.body));
} else {
done("Received incorrect body: " + JSON.stringify(res.body));
}
}
});
});
it('Should be able to get multiple times by multiple categories (JSON Method)', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments", {
json: {
videoID: "testtesttest",
categories: ["sponsor", "intro"]
}
},
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = res.body;
if (data.length === 2) {
let success = true;
for (const segment of data) {
if ((segment.segment[0] !== 20 || segment.segment[1] !== 33
|| segment.category !== "intro" || segment.UUID !== "1-uuid-2") &&
(segment.segment[0] !== 1 || segment.segment[1] !== 11
|| segment.category !== "sponsor" || segment.UUID !== "1-uuid-0")) {
success = false;
break;
}
}
if (success) done();
else done("Received incorrect body: " + JSON.stringify(res.body));
} else {
done("Received incorrect body: " + JSON.stringify(res.body));
}
}
});
});
it('Should be possible to send unexpected query parameters', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest&fakeparam=hello&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-0") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Low voted submissions should be hidden', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=test3&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-4") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
it('Should return 404 if no segment found', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=notarealvideo", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 404) done("non 404 respone code: " + res.statusCode);
else done(); // pass
});
});
it('Should be able send a comma in a query param', (done) => {
request.get(utils.getbaseURL()
+ "/api/skipSegments?videoID=testtesttest,test&category=sponsor", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("Status code was: " + res.statusCode);
else {
let data = JSON.parse(res.body);
if (data.length === 1 && data[0].segment[0] === 1 && data[0].segment[1] === 11
&& data[0].category === "sponsor" && data[0].UUID === "1-uuid-1") {
done();
} else {
done("Received incorrect body: " + res.body);
}
}
});
});
});

View file

@ -17,15 +17,15 @@ var utils = require('../utils.js');
);
*/
describe('getVideoSponsorTime', () => {
describe('getVideoSponsorTime (Old get method)', () => {
before(() => {
db.exec("INSERT INTO sponsorTimes VALUES ('testtesttest', 1, 11, 2, 'uuid-0', 'testman', 0, 50, 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('testtesttest,test', 1, 11, 2, 'uuid-1', 'testman', 0, 50, 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('old-testtesttest', 1, 11, 2, 'uuid-0', 'testman', 0, 50, 'sponsor', 0)");
db.exec("INSERT INTO sponsorTimes VALUES ('old-testtesttest,test', 1, 11, 2, 'uuid-1', 'testman', 0, 50, 'sponsor', 0)");
});
it('Should be able to get a time', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=testtesttest", null,
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200");
@ -46,7 +46,7 @@ describe('getVideoSponsorTime', () => {
it('Should be possible to send unexpected query parameters', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=testtesttest&fakeparam=hello", null,
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest&fakeparam=hello", null,
(err, res, body) => {
if (err) done("couldn't callendpoint");
else if (res.statusCode !== 200) done("non 200");
@ -56,7 +56,7 @@ describe('getVideoSponsorTime', () => {
it('Should be able send a comma in a query param', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=testtesttest,test", null,
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest,test", null,
(err, res, body) => {
if (err) done("couln't call endpoint");
else if (res.statusCode !== 200) done("non 200 response: " + res.statusCode);
@ -67,7 +67,7 @@ describe('getVideoSponsorTime', () => {
it('Should be able to get the correct time', (done) => {
request.get(utils.getbaseURL()
+ "/api/getVideoSponsorTimes?videoID=testtesttest", null,
+ "/api/getVideoSponsorTimes?videoID=old-testtesttest", null,
(err, res, body) => {
if (err) done("couldn't call endpoint");
else if (res.statusCode !== 200) done("non 200");

View file

@ -0,0 +1,55 @@
var assert = require('assert');
var request = require('request');
var utils = require('../utils.js');
var databases = require('../../src/databases/databases.js');
var db = databases.db;
describe('postVideoSponsorTime (Old submission method)', () => {
it('Should be able to submit a time (GET)', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=dQw4w9WgXcQ&startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare("SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?").get("dQw4w9WgXcQ");
if (row.startTime === 1 && row.endTime === 10 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit a time (POST)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=dQw4w9WgXcE&startTime=1&endTime=11&userID=test", null,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare("SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?").get("dQw4w9WgXcE");
if (row.startTime === 1 && row.endTime === 11 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should return 400 for missing params', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(err);
if (res.statusCode === 400) done();
else done("Status code was: " + res.statusCode);
});
});
});

View file

@ -0,0 +1,212 @@
var assert = require('assert');
var request = require('request');
var utils = require('../utils.js');
var databases = require('../../src/databases/databases.js');
var db = databases.db;
describe('postSkipSegments', () => {
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,
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare("SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?").get("dQw4w9WgXcR");
if (row.startTime === 2 && row.endTime === 10 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit a single time (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcF",
segments: [{
segment: [0, 10],
category: "sponsor"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let row = db.prepare("SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?").get("dQw4w9WgXcF");
if (row.startTime === 0 && row.endTime === 10 && row.category === "sponsor") {
done()
} else {
done("Submitted times were not saved. Actual submission: " + JSON.stringify(row));
}
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be able to submit multiple times (JSON method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ",
segments: [{
segment: [3, 10],
category: "sponsor"
}, {
segment: [30, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(err);
else if (res.statusCode === 200) {
let rows = db.prepare("SELECT startTime, endTime, category FROM sponsorTimes WHERE videoID = ?").all("dQw4w9WgXcR");
let success = true;
if (rows.length === 2) {
for (const row of rows) {
if ((row.startTime !== 3 || row.endTime !== 10 || row.category !== "sponsor") &&
(row.startTime !== 30 || row.endTime !== 60 || row.category !== "intro")) {
success = false;
break;
}
}
}
if (success) done();
else done("Submitted times were not saved. Actual submissions: " + JSON.stringify(row));
} else {
done("Status code was " + res.statusCode);
}
});
});
it('Should be rejected if over 80% of the video', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=qqwerty&startTime=30&endTime=1000000&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 403) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if not a valid videoID', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=knownWrongID&startTime=30&endTime=1000000&userID=testing", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 403) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should return 400 for missing params (Params method)', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=9&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done(true);
if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 1', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
segments: [{
segment: [9, 10],
category: "sponsor"
}, {
segment: [31, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 2', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ"
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 3', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ",
segments: [{
segment: [0],
category: "sponsor"
}, {
segment: [31, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 4', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ",
segments: [{
segment: [9, 10]
}, {
segment: [31, 60],
category: "intro"
}]
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
it('Should return 400 for missing params (JSON method) 5', (done) => {
request.post(utils.getbaseURL()
+ "/api/postVideoSponsorTimes", {
json: {
userID: "test",
videoID: "dQw4w9WgXcQ"
}
},
(err, res, body) => {
if (err) done(true);
else if (res.statusCode === 400) done();
else done(true);
});
});
});

View file

@ -1,47 +0,0 @@
var assert = require('assert');
var request = require('request');
var utils = require('../utils.js');
describe('postVideoSponsorTime', () => {
it('Should be able to create a time', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=fWvKvOViM3g&startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 200) done();
else done("non 200 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should return 400 for missing params', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?startTime=1&endTime=10&userID=test", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 400) done(); // pass
else done("non 400 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if over 80% of the video', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=qqwerty&startTime=1&endTime=1000000&userID=test", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 403) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
it('Should be rejected if not a valid videoID', (done) => {
request.get(utils.getbaseURL()
+ "/api/postVideoSponsorTimes?videoID=knownWrongID&startTime=1&endTime=1000000&userID=test", null,
(err, res, body) => {
if (err) done("Couldn't call endpoint");
else if (res.statusCode === 403) done(); // pass
else done("non 403 status code: " + res.statusCode + " ("+body+")");
});
});
});

View file

@ -1,8 +1,4 @@
BEGIN TRANSACTION;
DROP TABLE IF EXISTS "shadowBannedUsers";
DROP TABLE IF EXISTS "votes";
DROP TABLE IF EXISTS "sponsorTimes";
CREATE TABLE IF NOT EXISTS "shadowBannedUsers" (
"userID" TEXT NOT NULL
);

View file

@ -1,8 +1,4 @@
BEGIN TRANSACTION;
DROP TABLE IF EXISTS "vipUsers";
DROP TABLE IF EXISTS "sponsorTimes";
DROP TABLE IF EXISTS "userNames";
CREATE TABLE IF NOT EXISTS "vipUsers" (
"userID" TEXT NOT NULL
);
@ -15,6 +11,7 @@ CREATE TABLE IF NOT EXISTS "sponsorTimes" (
"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" (

View file

@ -4,12 +4,10 @@ var app = express();
var config = require('../src/config.js');
app.post('/ReportChannelWebhook', (req, res) => {
console.log("report mock hit");
res.status(200);
});
app.post('/FirstTimeSubmissionsWebhook', (req, res) => {
console.log("first time submisson mock hit");
res.status(200);
});

View file

@ -26,6 +26,14 @@ const YouTubeAPI = {
{
contentDetails: {
duration: "PT1H23M30S"
},
snippet: {
title: "Example Title",
thumbnails: {
maxres: {
url: "https://sponsor.ajay.app/LogoSponsorBlockSimple256px.png"
}
}
}
}
]