53 Commits
v0.2 ... 1.0.34

Author SHA1 Message Date
Ajay Ramachandran
0072cdb17b Merge pull request #18 from ajayyy/experimental
Username support
2019-08-12 23:43:17 -04:00
Ajay Ramachandran
6b88719cf6 Reverted postVideoSponsors back to get method 2019-08-12 23:43:05 -04:00
Ajay Ramachandran
4079419fa8 Added API endpoint to get the username 2019-08-12 23:31:44 -04:00
Ajay Ramachandran
dc33bc33d6 Added ability to set a username that is used when returning the stats. 2019-08-12 23:20:56 -04:00
Ajay Ramachandran
d94c2bdf95 Removed unnecessary comment 2019-08-12 13:19:56 -04:00
Ajay Ramachandran
43f658f5e9 Merge pull request #17 from ajayyy/experimental
Changed limits and better stats
2019-08-12 13:17:20 -04:00
Ajay Ramachandran
db4ddb0b8b Made low voted submissions not count in the stats 2019-08-12 12:42:27 -04:00
Ajay Ramachandran
073717cd1f Raised per user sponsor limit to 8 2019-08-12 12:34:51 -04:00
Ajay Ramachandran
29cb68ac31 Update README.MD 2019-08-03 22:43:14 -04:00
Ajay Ramachandran
b53495a0d2 Update README.MD 2019-08-03 22:41:46 -04:00
Ajay Ramachandran
363cc1da69 Update README.MD 2019-08-03 22:41:15 -04:00
Ajay Ramachandran
3d72a674e6 Update README.MD 2019-08-03 22:40:49 -04:00
Ajay Ramachandran
06f160d8ab Added API docs 2019-08-03 22:37:35 -04:00
Ajay Ramachandran
8c235f6fcc Merge pull request #15 from ajayyy/experimental
Added user count to stats
2019-08-03 15:10:19 -04:00
Ajay Ramachandran
6df7eed22a Added user count to stats 2019-08-03 15:10:04 -04:00
Ajay Ramachandran
6f07fbc536 Merge pull request #14 from ajayyy/experimental
Added totals api endpoint
2019-08-03 12:05:19 -04:00
Ajay Ramachandran
463a48f33a Added totals api endpoint 2019-08-03 12:04:22 -04:00
Ajay Ramachandran
f449d05a38 Merge pull request #13 from ajayyy/experimental
Raised stats limit to 50
2019-08-03 00:24:11 -04:00
Ajay Ramachandran
580a9d9eba Raised stats limit to 50 2019-08-03 00:23:43 -04:00
Ajay Ramachandran
094a2fb2a0 Merge pull request #12 from ajayyy/experimental
Added stats endpoint
2019-08-03 00:13:52 -04:00
Ajay Ramachandran
4dca4081c1 Added api endpoint to get the top users 2019-08-03 00:13:21 -04:00
Ajay Ramachandran
c9ccc409a3 Merge pull request #10 from OfficialNoob/patch-1
Added hash function and BehindProxy bool
2019-07-31 23:36:12 -04:00
Ajay Ramachandran
d5d33f0e9b Reformatted and fixed missing parameters. 2019-07-31 23:32:25 -04:00
Ajay Ramachandran
dfd8d84e85 Merge pull request #11 from ajayyy/experimental
Raised cutoff due to low amount of users
2019-07-30 19:32:28 -04:00
Ajay Ramachandran
f5794f1fc3 Raised cutoff due to low amount of users. 2019-07-30 19:31:56 -04:00
Official Noob
c67fb34588 Removed uuidv1 and added GetIP() 2019-07-30 18:43:23 +01:00
Official Noob
af1ae4346f Added hash function and BehindProxy bool 2019-07-30 18:14:25 +01:00
Ajay Ramachandran
9c132c5089 Merge pull request #9 from ajayyy/experimental
Privacy + Security Additions
2019-07-28 23:01:35 -04:00
Ajay Ramachandran
4e732b6367 Made votes anonymous. 2019-07-28 23:00:54 -04:00
Ajay Ramachandran
3720681f84 Made IP addresses private. 2019-07-28 22:58:20 -04:00
Ajay Ramachandran
2b16872936 Merge pull request #8 from ajayyy/experimental
Fixed NaN check not correct
2019-07-28 16:06:00 -04:00
Ajay Ramachandran
dadbf8026e Fixed NaN check not correct. 2019-07-28 16:05:23 -04:00
Ajay Ramachandran
fd6071f8d6 Removed extra comment. 2019-07-26 17:15:42 -04:00
Ajay Ramachandran
1148803671 Merge pull request #7 from ajayyy/experimental
Fixed NaN crashing the server
2019-07-26 15:20:56 -04:00
Ajay Ramachandran
4379660b01 Fixed NaN crashing the server. 2019-07-26 15:20:34 -04:00
Ajay Ramachandran
51efb9a5c1 Merge pull request #6 from ajayyy/experimental
Added hashing to userIDs and changed up how the UUID is created
2019-07-25 16:59:36 -04:00
Ajay Ramachandran
abfbba2ad0 Fixed server crash. 2019-07-25 16:56:06 -04:00
Ajay Ramachandran
7e041e5b49 Prevented backwards sponsor times. 2019-07-25 16:54:43 -04:00
Ajay Ramachandran
d7dec47de7 Made the UUID a hash of the input instead of random. 2019-07-25 16:48:13 -04:00
Ajay Ramachandran
71527cc4b1 Switched back to sha256, sha512 is just too long. 2019-07-25 16:36:53 -04:00
Ajay Ramachandran
5fbe580c08 Hash the userIDs 2019-07-25 16:35:08 -04:00
Ajay Ramachandran
c59372dd62 Merge pull request #5 from ajayyy/experimental
Viewcount monitoring
2019-07-23 20:07:09 -04:00
Ajay Ramachandran
ab0631ff63 Prevented sponsor segments less than 1 seconds from being submitted. 2019-07-23 19:06:43 -04:00
Ajay Ramachandran
db8c2e76e5 Updated posting to work with extra database column. 2019-07-23 18:31:42 -04:00
Ajay Ramachandran
11c099c3dc Made server save and report viewCount. 2019-07-23 17:59:19 -04:00
Ajay Ramachandran
9d0e479b90 Merge pull request #4 from ajayyy/experimental
Ratelimiting and improved IP hashing
2019-07-22 19:27:43 -04:00
Ajay Ramachandran
cd36e2b64b Made it run the hash function 5000 times to ensure no one will brute force the IPs. 2019-07-22 17:10:23 -04:00
Ajay Ramachandran
930c0bc6a3 Added rate limit per day per IP. 2019-07-21 22:06:01 -04:00
Ajay Ramachandran
5a5118a7b0 Made it order the sponsor times by start time. 2019-07-20 21:48:08 -04:00
Ajay Ramachandran
5e3c8a3b15 Fixed submissions being broken. 2019-07-20 15:29:31 -04:00
Ajay Ramachandran
a0e63e7326 Preventing voting twice accept for changing vote. 2019-07-19 17:00:50 -04:00
Ajay Ramachandran
27a6741d3e Update README.MD 2019-07-17 22:04:04 -04:00
Ajay Ramachandran
6850414a27 Replaced typeof with proper undefined check 2019-07-17 21:58:40 -04:00
2 changed files with 479 additions and 49 deletions

184
README.MD
View File

@@ -1,6 +1,6 @@
# SponsorBlocker Server
# SponsorBlock Server
SponsorBlocker is an extension that will skip over sponsored segments of YouTube videos. SponsorBlocker is a crowdsourced browser extension that let's anyone submit the start and end time's of sponsored segments of YouTube videos. Once one person submits this information, everyone else with this extension will skip right over the sponsored segment.
SponsorBlock is an extension that will skip over sponsored segments of YouTube videos. SponsorBlock is a crowdsourced browser extension that let's anyone submit the start and end time's of sponsored segments of YouTube videos. Once one person submits this information, everyone else with this extension will skip right over the sponsored segment.
This is the server backend for it
@@ -14,4 +14,182 @@ Hopefully this project can be combined with projects like [this](https://github.
# Client
The client web browser extension is available here: https://github.com/ajayyy/SponsorBlocker
The client web browser extension is available here: https://github.com/ajayyy/SponsorBlock
# API Docs
Public API available at https://sponsor.ajay.app.
________________________________________________________________________________
`/api/getVideoSponsorTimes`
**Input**:
```
{
videoID: string
}
```
**Response**:
```
{
sponorTimes: array [float],
UUIDs: array [string] //The ID for this sponsor time, used to submit votes
}
```
**Error codes**:
404: Not Found
__________________________________________________________________
`/api/postVideoSponsorTimes`
**Input**:
```
{
videoID: string,
startTime: float,
endTime: float,
userID: string //This should be a randomly generated UUID
}
```
**Response**:
```
{
Nothing (status code 200)
}
```
**Error codes**:
400: Bad Request (Your inputs are wrong/impossible)
429: Rate Limit (Too many for the same user or IP)
409: Duplicate
__________________________________________________________________
`/api/voteOnSponsorTime`
**Input**:
```
{
UUID: string, //id of the sponsor being voted on
userID: string,
type: int //0 for downvote, 1 for upvote
}
```
**Response**:
```
{
Nothing (status code 200)
}
```
**Error codes**:
400: Bad Request (Your inputs are wrong/impossible)
405: Duplicate
__________________________________________________________________
`/api/viewedVideoSponsorTime`
**Input**:
```
{
UUID: string
}
```
**Response**:
```
{
Nothing (status code 200
}
```
**Error codes**:
400: Bad Request (Your inputs are wrong/impossible)
__________________________________________________________________
`/api/getViewsForUser`
**Input**:
```
{
userID: string
}
```
**Response**:
```
{
viewCount: int
}
```
**Error codes**:
404: Not Found
__________________________________________________________________
### Stats Calls
`/api/getTopUsers`
**Input**:
```
{
sortType: int //0 for by minutes saved, 1 for by view count, 2 for by total submissions
}
```
**Response**:
```
{
userNames: array [string],
viewCounts: array [int],
totalSubmissions: array [int],
minutesSaved: array [float]
}
```
**Error codes**:
400: Bad Request (Your inputs are wrong/impossible)
__________________________________________________________________
`/api/getTotalStats`
**Input**:
```
{
Nothing
}
```
**Response**:
```
{
userCount: int,
viewCount: int,
totalSubmissions: int,
minutesSaved: float
}
```
**Error codes**:
None

344
index.js
View File

@@ -1,18 +1,16 @@
var express = require('express');
var http = require('http');
// Create a service (the app object is just a callback).
var app = express();
//uuid service
var uuidv1 = require('uuid/v1');
//hashing service
var crypto = require('crypto');
//load database
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('./databases/sponsorTimes.db');
//where the more sensitive data such as IP addresses are stored
var privateDB = new sqlite3.Database('./databases/private.db');
// Create an HTTP service.
http.createServer(app).listen(80);
@@ -21,6 +19,9 @@ http.createServer(app).listen(80);
// make it even harder for someone to decode the ip
var globalSalt = "49cb0d52-1aec-4b89-85fc-fab2c53062fb";
//if so, it will use the x-forwarded header instead of the ip address of the connection
var behindProxy = true;
//setup CORS correctly
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
@@ -36,12 +37,12 @@ app.get('/api/getVideoSponsorTimes', function (req, res) {
let votes = []
let UUIDs = [];
db.prepare("SELECT startTime, endTime, votes, UUID FROM sponsorTimes WHERE videoID = ?").all(videoID, function(err, rows) {
db.prepare("SELECT startTime, endTime, votes, UUID FROM sponsorTimes WHERE videoID = ? ORDER BY startTime").all(videoID, function(err, rows) {
if (err) console.log(err);
for (let i = 0; i < rows.length; i++) {
//check if votes are above -2
if (rows[i].votes < -2) {
//check if votes are above -1
if (rows[i].votes < -1) {
//too untrustworthy, just ignore it
continue;
}
@@ -77,6 +78,10 @@ app.get('/api/getVideoSponsorTimes', function (req, res) {
});
});
function getIP(req) {
return behindProxy ? req.headers['x-forwarded-for'] : req.connection.remoteAddress;
}
//add the post function
app.get('/api/postVideoSponsorTimes', function (req, res) {
let videoID = req.query.videoID;
@@ -84,44 +89,74 @@ app.get('/api/postVideoSponsorTimes', function (req, res) {
let endTime = req.query.endTime;
let userID = req.query.userID;
if (typeof videoID != 'string' || startTime == undefined || endTime == undefined || userID == undefined) {
//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;
}
//x-forwarded-for if this server is behind a proxy
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
//hash the ip so no one can get it from the database
let hashCreator = crypto.createHash('sha256');
let hashedIP = hashCreator.update(ip + globalSalt).digest('hex');
//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) + globalSalt);
startTime = parseFloat(startTime);
endTime = parseFloat(endTime);
let UUID = uuidv1();
if (isNaN(startTime) || isNaN(endTime)) {
//invalid request
res.sendStatus(400);
return;
}
if (startTime > endTime) {
//time can't go backwards
res.sendStatus(400);
return;
}
//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 hashCreator = crypto.createHash('sha256');
let UUID = hashCreator.update(videoID + startTime + endTime + userID).digest('hex');
//get current time
let timeSubmitted = Date.now();
//check to see if the user has already submitted sponsors for this video
db.prepare("SELECT UUID FROM sponsorTimes WHERE userID = ? and videoID = ?").all([userID, videoID], function(err, rows) {
if (rows.length >= 4) {
//too many sponsors for the same video from the same user
let yesterday = timeSubmitted - 86400000;
//check to see if this ip has submitted too many sponsors today
privateDB.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE hashedIP = ? AND videoID = ? AND timeSubmitted > ?").get([hashedIP, videoID, yesterday], function(err, row) {
if (row.count >= 10) {
//too many sponsors for the same video from the same ip address
res.sendStatus(429);
} else {
//check if this info has already been submitted first
db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID], function(err, row) {
if (err) console.log(err);
if (row == null) {
//not a duplicate, execute query
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, startTime, endTime, 0, UUID, userID, hashedIP, timeSubmitted);
res.sendStatus(200);
//check to see if the user has already submitted sponsors for this video
db.prepare("SELECT COUNT(*) as count FROM sponsorTimes WHERE userID = ? and videoID = ?").get([userID, videoID], function(err, row) {
if (row.count >= 8) {
//too many sponsors for the same video from the same user
res.sendStatus(429);
} else {
res.sendStatus(409);
//check if this info has already been submitted first
db.prepare("SELECT UUID FROM sponsorTimes WHERE startTime = ? and endTime = ? and videoID = ?").get([startTime, endTime, videoID], function(err, row) {
if (err) console.log(err);
if (row == null) {
//not a duplicate, execute query
db.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?, ?, ?, ?, ?, ?)").run(videoID, startTime, endTime, 0, UUID, userID, timeSubmitted, 0);
//add to private db as well
privateDB.prepare("INSERT INTO sponsorTimes VALUES(?, ?, ?)").run(videoID, hashedIP, timeSubmitted);
res.sendStatus(200);
} else {
res.sendStatus(409);
}
});
}
});
}
@@ -140,37 +175,244 @@ app.get('/api/voteOnSponsorTime', function (req, res) {
return;
}
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
let incrementAmount = 0;
//hash the userID
userID = getHash(userID + UUID);
//don't use userID for now, and just add the vote
if (type == 1) {
//upvote
incrementAmount = 1;
} else if (type == 0) {
//downvote
incrementAmount = -1;
} else {
//unrecongnised type of vote
req.sendStatus(400);
//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 + globalSalt);
//check if vote has already happened
privateDB.prepare("SELECT type FROM votes WHERE userID = ? AND UUID = ?").get(userID, UUID, function(err, row) {
if (err) console.log(err);
if (row != undefined && row.type == type) {
//they have already done this exact vote
res.status(405).send("Duplicate Vote");
return;
}
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
let incrementAmount = 0;
let oldIncrementAmount = 0;
if (type == 1) {
//upvote
incrementAmount = 1;
} else if (type == 0) {
//downvote
incrementAmount = -1;
} else {
//unrecongnised type of vote
res.sendStatus(400);
return;
}
if (row != undefined) {
if (row.type == 1) {
//upvote
oldIncrementAmount = 1;
} else if (row.type == 0) {
//downvote
oldIncrementAmount = -1;
}
}
//update the votes table
if (row != 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);
//added to db
res.sendStatus(200);
});
});
//Endpoint when a sponsorTime is used up
app.get('/api/viewedVideoSponsorTime', function (req, res) {
let UUID = req.query.UUID;
if (UUID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
db.prepare("UPDATE sponsorTimes SET votes = votes + ? WHERE UUID = ?").run(incrementAmount, UUID);
//up the view count by one
db.prepare("UPDATE sponsorTimes SET views = views + 1 WHERE UUID = ?").run(UUID);
//added to db
res.sendStatus(200);
});
//To set your username for the stats view
app.post('/api/setUsername', function (req, res) {
let userID = req.query.userID;
let userName = req.query.username;
if (userID == undefined || userName == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
//check if username is already set
db.prepare("SELECT count(*) as count FROM userNames WHERE userID = ?").get(userID, function(err, row) {
if (err) console.log(err);
if (row.count > 0) {
//already exists, update this row
db.prepare("UPDATE userNames SET userName = ? WHERE userID = ?").run(userName, userID);
} else {
//add to the db
db.prepare("INSERT INTO userNames VALUES(?, ?)").run(userID, userName);
}
res.sendStatus(200);
});
});
//get what username this user has
app.get('/api/getUsername', function (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
db.prepare("SELECT userName FROM userNames WHERE userID = ?").get(userID, function(err, row) {
if (err) console.log(err);
if (row != null) {
res.send({
userName: row.userName
});
} else {
//no username yet, just send back the userID
res.send({
userName: userID
});
}
});
});
//Gets all the views added up for one userID
//Useful to see how much one user has contributed
app.get('/api/getViewsForUser', function (req, res) {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
//up the view count by one
db.prepare("SELECT SUM(views) as viewCount FROM sponsorTimes WHERE userID = ?").get(userID, function(err, row) {
if (err) console.log(err);
if (row.viewCount != null) {
res.send({
viewCount: row.viewCount
});
} else {
res.sendStatus(404);
}
});
});
app.get('/api/getTopUsers', function (req, res) {
let sortType = req.query.sortType;
if (sortType == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//setup which sort type to use
let sortBy = "";
if (sortType == 0) {
sortBy = "minutesSaved";
} else if (sortType == 1) {
sortBy = "viewCount";
} else if (sortType == 2) {
sortBy = "totalSubmissions";
} else {
//invalid request
res.sendStatus(400);
return;
}
let userNames = [];
let viewCounts = [];
let totalSubmissions = [];
let minutesSaved = [];
db.prepare("SELECT sponsorTimes.userID as userID, COUNT(*) as totalSubmissions, SUM(views) as viewCount, SUM((sponsorTimes.endTime - sponsorTimes.startTime) / 60 * sponsorTimes.views) as minutesSaved, userNames.userName as userName FROM sponsorTimes LEFT JOIN userNames ON sponsorTimes.userID=userNames.userID WHERE sponsorTimes.votes > -1 GROUP BY sponsorTimes.userID ORDER BY " + sortBy + " DESC LIMIT 50").all(function(err, rows) {
for (let i = 0; i < rows.length; i++) {
if (rows[i].userName != null) {
userNames[i] = rows[i].userName;
} else {
userNames[i] = rows[i].userID;
}
viewCounts[i] = rows[i].viewCount;
totalSubmissions[i] = rows[i].totalSubmissions;
minutesSaved[i] = rows[i].minutesSaved;
}
//send this result
res.send({
userNames: userNames,
viewCounts: viewCounts,
totalSubmissions: totalSubmissions,
minutesSaved: minutesSaved
});
});
});
//send out totals
//send the total submissions, total views and total minutes saved
app.get('/api/getTotalStats', function (req, res) {
db.prepare("SELECT COUNT(DISTINCT userID) as userCount, COUNT(*) as totalSubmissions, SUM(views) as viewCount, SUM((endTime - startTime) / 60 * views) as minutesSaved FROM sponsorTimes").get(function(err, row) {
if (row != null) {
//send this result
res.send({
userCount: row.userCount,
viewCount: row.viewCount,
totalSubmissions: row.totalSubmissions,
minutesSaved: row.minutesSaved
});
}
});
});
app.get('/database.db', function (req, res) {
res.sendFile("./databases/sponsorTimes.db", { root: __dirname });
});
//This function will find sponsor times that are contained inside of eachother, called similar sponsor times
//Only one similar time will be returned, randomly generated based on the sqrt of votes.
//This allows new less voted items to still sometimes appear to give them a chance at getting votes.
//Sponsor times with less than -2 votes are already ignored before this function is called
//Sponsor times with less than -1 votes are already ignored before this function is called
function getVoteOrganisedSponsorTimes(sponsorTimes, votes, UUIDs) {
//list of sponsors that are contained inside eachother
let similarSponsors = [];
@@ -327,6 +569,7 @@ function getWeightedRandomChoice(choices, weights, amountOfChoices) {
//iterate and find amountOfChoices choices
let randomNumber = Math.random();
//this array will keep adding to this variable each time one sqrt vote has been dealt with
//this is the sum of all the sqrtVotes under this index
let currentVoteNumber = 0;
@@ -356,4 +599,13 @@ function getWeightedRandomChoice(choices, weights, amountOfChoices) {
finalChoices: finalChoices,
choicesDealtWith: choicesDealtWith
};
}
}
function getHash(value, times=5000) {
for (let i = 0; i < times; i++) {
let hashCreator = crypto.createHash('sha256');
value = hashCreator.update(value).digest('hex');
}
return value;
}