Skip to content

Commit

Permalink
Add proper ratelimiting, and reorganise some things
Browse files Browse the repository at this point in the history
  • Loading branch information
Ovyerus committed Oct 17, 2017
1 parent 593624e commit b21115c
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 94 deletions.
5 changes: 1 addition & 4 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,4 @@ engines:
enabled: true
ratings:
paths:
- "lib/**.js"
exclude_paths:
- data/*
- assets/*
- "lib/**.js"
14 changes: 13 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
module.exports = require('./lib/Sagiri');
const Handler = require('./lib/Handler');
const Constants = require('./lib/Constants');
const Ratelimiter = require('./lib/Ratelimiter');

function Sagiri(key, options) {
return new Handler(key, options);
}

Sagiri.Handler = Handler;
Sagiri.Constants = Constants;
Sagiri.Ratelimiter = Ratelimiter;

module.exports = Sagiri;
31 changes: 13 additions & 18 deletions lib/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ const SITE_LIST = {
name: 'Danbooru',
backupURL: data => `https://danbooru.donmai.us/posts/${data.data.danbooru_id}`,
URLRegex: /(?:https?:\/\/)?danbooru\.donmai\.us\/(?:posts|post\/show)\/\d{7}/i,
getRating(body) {
let rating = body.match(/<li>Rating: (.*?)<\/li>/i)[1];

return [null, 'Safe', 'Questionable', 'Explicit'].indexOf(rating);
}
getRating: body => [null, 'Safe', 'Questionable', 'Explicit'].indexOf(body.match(/<li>Rating: (.*?)<\/li>/i)[1])
},
10: {
name: 'drawr',
Expand All @@ -38,21 +34,19 @@ const SITE_LIST = {
name: 'Yande.re',
backupURL: data => `https://yande.re/post/show/${data.data.yandere_id}`,
URLRegex: /(?:https?:\/\/)?yande\.re\/post\/show\/\d{6}/i,
getRating(body) {
let rating = body.match(/<li>Rating: (.*?)<\/li>/i)[1].split(' ')[0];

return [null, 'Safe', 'Questionable', 'Explicit'].indexOf(rating);
}
getRating: body => [null, 'Safe', 'Questionable', 'Explicit'].indexOf(body.match(/<li>Rating: (.*?)<\/li>/i)[1])
},
16: {
name: 'FAKKU',
backupURL: data => `https://www.fakku.net/hentai/${data.data.source.toLowerCase().replace(' ', '-')}`,
URLRegex: /(?:https?:\/\/)?(www\.)?fakku\.net\/hentai\/[a-z-]+\d{10}/i
URLRegex: /(?:https?:\/\/)?(www\.)?fakku\.net\/hentai\/[a-z-]+\d{10}/i,
getRating: () => RATINGS.NSFW
},
18: {
name: 'nHentai',
backupURL: data => `https://nhentai.net/g/${data.header.thumbnail.match(/nhentai\/(\d+)/)[1]}`,
URLRegex: /(?:https?:\/\/)nhentai.net\/g\/\d+/i
URLRegex: /(?:https?:\/\/)nhentai.net\/g\/\d+/i,
getRating: () => RATINGS.NSFW
},
19: {
name: '2D-Market',
Expand All @@ -78,11 +72,7 @@ const SITE_LIST = {
name: 'Gelbooru',
backupURL: data => `https://gelbooru.com/index.php?page=post&s=view&id=${data.data.gelbooru_id}`,
URLRegex: /(?:https?:\/\/)gelbooru\.com\/index\.php\?page=post&s=view&id=\d{7}/i,
getRating(body) {
let rating = body.match(/<li>Rating: (.*?)<\/li>/i)[1];

return [null, 'Safe', 'Questionable', 'Explicit'].indexOf(rating);
}
getRating: body => [null, 'Safe', 'Questionable', 'Explicit'].indexOf(body.match(/<li>Rating: (.*?)<\/li>/i)[1])
},
26: {
name: 'Konachan',
Expand Down Expand Up @@ -151,4 +141,9 @@ const RATINGS = Object.freeze({
NSFW: 3
});

module.exports = {SITE_LIST, RATINGS};
const PERIODS = Object.freeze({
SHORT: 1000 * 30, // 30 seconds
LONG: 1000 * 60 * 60 * 24 // 24 hours
});

module.exports = {SITE_LIST, RATINGS, PERIODS};
123 changes: 59 additions & 64 deletions lib/Sagiri.js → lib/Handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@ const FormData = require('form-data');
const fs = require('fs');
const http = require('http');
const https = require('https');
const {SITE_LIST, RATINGS} = require('./Constants');
const {SITE_LIST, RATINGS, PERIODS} = require('./Constants');
const Ratelimiter = require('./Ratelimiter');

/**
* Query handler for SauceNAO.
*
* @prop {String} key API key
* @prop {Number} numRes Amount of responses returned from the API.
* @prop {?Number} shortLimit Ratelimit for the "short" period, currently the last 30 seconds. Will be null before the first request.
* @prop {?Number} longLimit Ratelimit for the "long" period, currently 24 hours. Will be null before the first request.
* @prop {?Number} shortRemaining Amount of requests left during the "short" period before you get ratelimited. Will be null before the first request.
* @prop {?Number} longRemaining Amount of requests left during the "long" period before you get ratelimited. Will be null before the first request.
*/
class Sagiri {
class Handler {
/**
* @param {String} key API Key for SauceNAO
* @param {Object} [options] Optional options
Expand All @@ -34,10 +33,8 @@ class Sagiri {
this.key = key,
this.numRes = options.numRes != null ? options.numRes : 5;
this.getRating = options.getRating || false;
this.shortLimit = null;
this.longLimit = null;
this.shortRemaining = null;
this.longRemaining = null;
this.shortLimiter = new Ratelimiter(20, PERIODS.SHORT); // 20 uses every 30 seconds
this.longLimiter = new Ratelimiter(300, PERIODS.SHORT); // 300 uses every 24 hours
}

/**
Expand All @@ -49,61 +46,59 @@ class Sagiri {
*/
getSauce(file) {
return new Promise((resolve, reject) => {
if (typeof file !== 'string') {
reject(new Error('file is not a string.'));
} else {
let form = new FormData();

form.append('api_key', this.key);
form.append('output_type', 2);
form.append('numres', this.numRes);

if (fs.existsSync(file)) form.append('file', fs.createReadStream(file));
else form.append('url', file);

sendForm(form).then(res => {
this.shortLimit = res.header.short_limit;
this.longLimit = res.header.long_limit;
this.shortRemaining = res.header.short_remaining;
this.longRemaining = res.header.long_remaining;

if (this.shortLimit === 0) throw new Error('Short duration ratelimit exceeded.');
if (this.longLimit === 0) throw new Error('Long duration ratelimit exceeded.');

if (res.header.status > 0) throw new Error(`Server-side error occurred. Error Code: ${res.header.status}`);
if (res.header.status < 0) throw new Error(`Client-side error occurred. Error code: ${res.header.status}`);

if (res.results.length === 0) throw new Error('No results.');

let results = res.results.sort((a, b) => Number(b.header.similarity) - Number(a.header.similarity));
let returnData = [];

for (let result of results) {
let data = resolveSauceData(result);
returnData.push({
url: data.url,
site: data.name,
index: data.id,
similarity: Number(result.header.similarity),
thumbnail: result.header.thumbnail,
rating: RATINGS.UNKNOWN,
original: result
});
}

return returnData;
}).then(res => {
if (!this.getRating) return res;
return Promise.all([Promise.all(res.map(v => getRating(v.url))), res]);
}).then(res => {
if (!this.getRating) return res;

let [ratings, original] = res;

ratings.forEach((v, i) => original[i].rating = v);
return original;
}).then(resolve).catch(reject);
}
if (typeof file !== 'string') return reject(new Error('file is not a string.'));
if (this.shortLimiter.ratelimited) return reject(new Error('Short duration ratelimit exceeded.'));
if (this.longLimiter.ratelimited) return reject(new Error('Long duration ratelimit exceeded.'));

let form = new FormData();

form.append('api_key', this.key);
form.append('output_type', 2);
form.append('numres', this.numRes);

if (fs.existsSync(file)) form.append('file', fs.createReadStream(file));
else form.append('url', file);

sendForm(form).then(res => {
if (Number(res.header.short_limit) !== this.shortLimiter.totalUses) this.shortLimiter.totalUses = Number(res.header.short_limit);
if (Number(res.header.long_limit) !== this.longLimiter.totalUses) this.longLimiter.totalUses = Number(res.header.long_limit);

this.shortLimiter.use();
this.longLimiter.use();

if (res.header.status > 0) throw new Error(`Server-side error occurred. Error Code: ${res.header.status}`);
if (res.header.status < 0) throw new Error(`Client-side error occurred. Error code: ${res.header.status}`);

if (res.results.length === 0) throw new Error('No results.');

let results = res.results.sort((a, b) => Number(b.header.similarity) - Number(a.header.similarity));
let returnData = [];

for (let result of results) {
let data = resolveSauceData(result);
returnData.push({
url: data.url,
site: data.name,
index: data.id,
similarity: Number(result.header.similarity),
thumbnail: result.header.thumbnail,
rating: RATINGS.UNKNOWN,
original: result
});
}

return returnData;
}).then(res => {
if (!this.getRating) return res;
return Promise.all([Promise.all(res.map(v => getRating(v.url))), res]);
}).then(res => {
if (!this.getRating) return res;

let [ratings, original] = res;

ratings.forEach((v, i) => original[i].rating = v);
return original;
}).then(resolve).catch(reject);
});
}

Expand Down Expand Up @@ -185,4 +180,4 @@ function getRating(url) {
});
}

module.exports = Sagiri;
module.exports = Handler;
37 changes: 37 additions & 0 deletions lib/Ratelimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Ratelimiter class.
*
* @prop {Number} totalUses Amount of times a entity can be used before being ratelimited.
* @prop {Number} interval Interval between resetting amount of uses.
* @prop {Number} uses Number of current uses this interval has.
*/
class Ratelimiter {
/**
* Constructs a new ratelimiter.
*
* @param {Number} totalUses The total amount of uses the ratelimiter can be used before
* @param {Number} interval Time in milliseoncds between resettings uses.
*/
constructor(totalUses, interval) {
if (typeof totalUses !== 'number') throw new TypeError('totalUses is not not a number.');
if (typeof interval !== 'number') throw new TypeError('interval is not not a number.');

this.totalUses = totalUses;
this.interval = interval;
this.uses = 0;
this._timer = setInterval(() => this.uses = 0, interval);
}

/**
* Add a use to the ratelimiter.
*/
use() {
if (this.uses < this.totalUses) this.uses++;
}

get ratelimited() {
return this.uses === this.totalUses;
}
}

module.exports = Ratelimiter;
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sagiri",
"version": "1.2.5",
"description": "A wrapper for the SauceNAO API.",
"version": "1.3.0",
"description": "A simple, lightweight and actually good JS wrapper for the SauceNAO API.",
"main": "index.js",
"repository": {
"type": "git",
Expand All @@ -10,7 +10,10 @@
"keywords": [
"hitorigoto",
"sagiri",
"saucenao"
"saucenao",
"simple",
"easy",
"api"
],
"author": "sr229",
"license": "MIT",
Expand All @@ -27,10 +30,6 @@
"devDependencies": {
"eslint": "^4.7.2"
},
"bin": {
"Sagiri.js": "lib/Sagiri.js",
"Constants.js": "lib/Constants.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
Expand Down

0 comments on commit b21115c

Please sign in to comment.