migrate to typescript

This commit is contained in:
Dainius Daukševičius
2020-10-17 21:56:54 +03:00
committed by Dainius Dauksevicius
parent c462323dd5
commit 08d27265fc
120 changed files with 5002 additions and 4711 deletions

View File

@@ -1,44 +0,0 @@
module.exports = function createMemoryCache(memoryFn, cacheTimeMs) {
if (isNaN(cacheTimeMs)) cacheTimeMs = 0;
// holds the promise results
const cache = new Map();
// holds the promises that are not fulfilled
const promiseMemory = new Map();
return (...args) => {
// create cacheKey by joining arguments as string
const cacheKey = args.join('.');
// check if promising is already running
if (promiseMemory.has(cacheKey)) {
return promiseMemory.get(cacheKey);
}
else {
// check if result is in cache
if (cache.has(cacheKey)) {
const cacheItem = cache.get(cacheKey);
const now = Date.now();
// check if cache is valid
if (!(cacheItem.cacheTime + cacheTimeMs < now)) {
return Promise.resolve(cacheItem.result);
}
}
// create new promise
const promise = new Promise(async (resolve, reject) => {
resolve((await memoryFn(...args)));
});
// store promise reference until fulfilled
promiseMemory.set(cacheKey, promise);
return promise.then(result => {
// store promise result in cache
cache.set(cacheKey, {
result,
cacheTime: Date.now(),
});
// remove fulfilled promise from memory
promiseMemory.delete(cacheKey);
// return promise result
return result;
});
}
};
};

View File

@@ -0,0 +1,43 @@
export function createMemoryCache(memoryFn: (...args: any[]) => void, cacheTimeMs: number) {
if (isNaN(cacheTimeMs)) cacheTimeMs = 0;
// holds the promise results
const cache = new Map();
// holds the promises that are not fulfilled
const promiseMemory = new Map();
return (...args: any[]) => {
// create cacheKey by joining arguments as string
const cacheKey = args.join('.');
// check if promising is already running
if (promiseMemory.has(cacheKey)) {
return promiseMemory.get(cacheKey);
} else {
// check if result is in cache
if (cache.has(cacheKey)) {
const cacheItem = cache.get(cacheKey);
const now = Date.now();
// check if cache is valid
if (!(cacheItem.cacheTime + cacheTimeMs < now)) {
return Promise.resolve(cacheItem.result);
}
}
// create new promise
const promise = new Promise(async (resolve) => {
resolve((await memoryFn(...args)));
});
// store promise reference until fulfilled
promiseMemory.set(cacheKey, promise);
return promise.then(result => {
// store promise result in cache
cache.set(cacheKey, {
result,
cacheTime: Date.now(),
});
// remove fulfilled promise from memory
promiseMemory.delete(cacheKey);
// return promise result
return result;
});
}
};
}

View File

@@ -1,14 +0,0 @@
//converts time in seconds to minutes:seconds
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;
}
let formatted = minutes+ ":" + secondsDisplay;
return formatted;
}

View File

@@ -0,0 +1,14 @@
/**
* Converts time in seconds to minutes:seconds
*/
export function getFormattedTime(totalSeconds: number) {
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;
}
return minutes + ':' + secondsDisplay;
}

View File

@@ -1,12 +0,0 @@
var crypto = require('crypto');
module.exports = function (value, times=5000) {
if (times <= 0) return "";
for (let i = 0; i < times; i++) {
let hashCreator = crypto.createHash('sha256');
value = hashCreator.update(value).digest('hex');
}
return value;
}

12
src/utils/getHash.ts Normal file
View File

@@ -0,0 +1,12 @@
import crypto from 'crypto';
export function getHash(value: string, times = 5000) {
if (times <= 0) return "";
for (let i = 0; i < times; i++) {
let hashCreator = crypto.createHash('sha256');
value = hashCreator.update(value).digest('hex');
}
return value;
}

View File

@@ -1,16 +0,0 @@
var config = require('../config.js');
module.exports = function getIP(req) {
if (config.behindProxy === true || config.behindProxy === "true") config.behindProxy = "X-Forwarded-For";
switch (config.behindProxy) {
case "X-Forwarded-For":
return req.headers['x-forwarded-for'];
case "Cloudflare":
return req.headers['cf-connecting-ip'];
case "X-Real-IP":
return req.headers['x-real-ip'];
default:
return req.connection.remoteAddress;
}
}

19
src/utils/getIP.ts Normal file
View File

@@ -0,0 +1,19 @@
import {config} from '../config';
import {Request} from 'express';
export function getIP(req: Request): string {
if (config.behindProxy === true || config.behindProxy === "true") {
config.behindProxy = "X-Forwarded-For";
}
switch (config.behindProxy as string) {
case "X-Forwarded-For":
return req.headers['x-forwarded-for'] as string;
case "Cloudflare":
return req.headers['cf-connecting-ip'] as string;
case "X-Real-IP":
return req.headers['x-real-ip'] as string;
default:
return req.connection.remoteAddress;
}
}

View File

@@ -1,7 +0,0 @@
const getHash = require('./getHash.js');
module.exports = function getSubmissionUUID(videoID, category, userID,
startTime, endTime) {
return getHash('v2-categories' + videoID + startTime + endTime + category +
userID, 1);
};

View File

@@ -0,0 +1,5 @@
import {getHash} from './getHash';
export function getSubmissionUUID(videoID: string, category: string, userID: string, startTime: number, endTime: number) {
return getHash('v2-categories' + videoID + startTime + endTime + category + userID, 1);
}

View File

@@ -1,10 +1,10 @@
const config = require('../config.js');
import {config} from '../config';
const minimumPrefix = config.minimumPrefix || '3';
const maximumPrefix = config.maximumPrefix || '32'; // Half the hash.
const prefixChecker = new RegExp('^[\\da-f]{' + minimumPrefix + ',' + maximumPrefix + '}$', 'i');
module.exports = (prefix) => {
export function hashPrefixTester(prefix: string): boolean {
return prefixChecker.test(prefix);
};
}

View File

@@ -1,20 +0,0 @@
var databases = require('../databases/databases.js');
var db = databases.db;
//returns true if the user is considered trustworthy
//this happens after a user has made 5 submissions and has less than 60% downvoted submissions
module.exports = async (userID) => {
//check to see if this user how many submissions this user has submitted
let totalSubmissionsRow = db.prepare('get', "SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?", [userID]);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
let downvotedSubmissionsRow = db.prepare('get', "SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)", [userID]);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}

View File

@@ -0,0 +1,20 @@
import {db} from '../databases/databases';
/**
* Returns true if the user is considered trustworthy. This happens after a user has made 5 submissions and has less than 60% downvoted submissions
* @param userID
*/
export async function isUserTrustworthy(userID: string): Promise<boolean> {
//check to see if this user how many submissions this user has submitted
const totalSubmissionsRow = db.prepare('get', "SELECT count(*) as totalSubmissions, sum(votes) as voteSum FROM sponsorTimes WHERE userID = ?", [userID]);
if (totalSubmissionsRow.totalSubmissions > 5) {
//check if they have a high downvote ratio
const downvotedSubmissionsRow = db.prepare('get', "SELECT count(*) as downvotedSubmissions FROM sponsorTimes WHERE userID = ? AND (votes < 0 OR shadowHidden > 0)", [userID]);
return (downvotedSubmissionsRow.downvotedSubmissions / totalSubmissionsRow.totalSubmissions) < 0.6 ||
(totalSubmissionsRow.voteSum > downvotedSubmissionsRow.downvotedSubmissions);
}
return true;
}

View File

@@ -1,7 +1,6 @@
const databases = require('../databases/databases.js');
const db = databases.db;
import {db} from '../databases/databases';
module.exports = (userID) => {
export function isUserVIP(userID: string): boolean {
return db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
}

View File

@@ -1,70 +0,0 @@
const config = require('../config.js');
const levels = {
ERROR: "ERROR",
WARN: "WARN",
INFO: "INFO",
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,
INFO: false,
DEBUG: false
};
if (config.mode === 'development') {
settings.INFO = true;
settings.DEBUG = true;
} else if (config.mode === 'test') {
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(colors.Dim, level + " " + new Date().toISOString() + ": ", color, string, colors.Reset);
}
}
module.exports = {
levels,
log,
error: (string) => {log(levels.ERROR, string)},
warn: (string) => {log(levels.WARN, string)},
info: (string) => {log(levels.INFO, string)},
debug: (string) => {log(levels.DEBUG, string)},
};

89
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,89 @@
import {config} from '../config';
const enum LogLevel {
ERROR = "ERROR",
WARN = "WARN",
INFO = "INFO",
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",
};
class Logger {
private _settings = {
ERROR: true,
WARN: true,
INFO: false,
DEBUG: false,
};
constructor() {
if (config.mode === 'development') {
this._settings.INFO = true;
this._settings.DEBUG = true;
} else if (config.mode === 'test') {
this._settings.WARN = false;
}
}
error(str: string) {
this.log(LogLevel.ERROR, str);
}
warn(str: string) {
this.log(LogLevel.WARN, str);
}
info(str: string) {
this.log(LogLevel.INFO, str);
}
debug(str: string) {
this.log(LogLevel.DEBUG, str);
}
private log(level: LogLevel, str: string) {
if (!this._settings[level]) {
return;
}
let color = colors.Bright;
if (level === LogLevel.ERROR) color = colors.FgRed;
if (level === LogLevel.WARN) color = colors.FgYellow;
let levelStr = level.toString();
if (levelStr.length === 4) {
levelStr += " "; // ensure logs are aligned
}
console.log(colors.Dim, `${levelStr} ${new Date().toISOString()}: `, color, str, colors.Reset);
}
}
const loggerInstance = new Logger();
export {
loggerInstance as Logger
}

View File

@@ -1,18 +0,0 @@
const config = require('../config.js');
const logger = require('./logger.js');
if (config.redis) {
const redis = require('redis');
logger.info('Connected to redis');
const client = redis.createClient(config.redis);
module.exports = client;
} else {
module.exports = {
get: (key, callback) => {
callback(false);
},
set: (key, value, callback) => {
callback(false);
}
};
}

19
src/utils/redis.ts Normal file
View File

@@ -0,0 +1,19 @@
import {config} from '../config';
import {Logger} from './logger';
import redis, {Callback} from 'redis';
let get, set;
if (config.redis) {
Logger.info('Connected to redis');
const client = redis.createClient(config.redis);
get = client.get;
set = client.set;
} else {
get = (key: string, callback?: Callback<string | null>) => callback(null, undefined);
set = (key: string, value: string, callback?: Callback<string | null>) => callback(null, undefined);
}
export {
get,
set,
};

View File

@@ -1,52 +0,0 @@
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 (isOwnSubmission) {
return "Report by Submitter";
} else if (isVIP) {
return "Report by VIP User";
}
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
}

56
src/utils/webhookUtils.ts Normal file
View File

@@ -0,0 +1,56 @@
import {config} from '../config';
import {Logger} from '../utils/logger';
import request from 'request';
function getVoteAuthorRaw(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string {
if (isOwnSubmission) {
return "self";
} else if (isVIP) {
return "vip";
} else if (submissionCount === 0) {
return "new";
} else {
return "other";
}
}
function getVoteAuthor(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string {
if (submissionCount === 0) {
return "Report by New User";
} else if (isOwnSubmission) {
return "Report by Submitter";
} else if (isVIP) {
return "Report by VIP User";
}
return "";
}
function dispatchEvent(scope: string, data: any): void {
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;
// TODO TYPESCRIPT deprecated
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.message);
});
});
}
export {
getVoteAuthorRaw,
getVoteAuthor,
dispatchEvent,
};

View File

@@ -1,62 +0,0 @@
var config = require('../config.js');
// YouTube API
const YouTubeAPI = require("youtube-api");
const redis = require('./redis.js');
const logger = require('./logger.js');
var exportObject;
// If in test mode, return a mocked youtube object
// otherwise return an authenticated youtube api
if (config.mode === "test") {
exportObject = require("../../test/youtubeMock.js");
} else {
YouTubeAPI.authenticate({
type: "key",
key: config.youtubeAPIKey
});
exportObject = YouTubeAPI;
// YouTubeAPI.videos.list wrapper with cacheing
exportObject.listVideos = (videoID, callback) => {
let part = 'contentDetails,snippet';
if (videoID.length !== 11 || videoID.includes(".")) {
callback("Invalid video ID");
return;
}
let redisKey = "youtube.video." + videoID;
redis.get(redisKey, (getErr, result) => {
if (getErr || !result) {
logger.debug("redis: no cache for video information: " + videoID);
YouTubeAPI.videos.list({
part,
id: videoID
}, (ytErr, data) => {
if (!ytErr) {
// Only set cache if data returned
if (data.items.length > 0) {
redis.set(redisKey, JSON.stringify(data), (setErr) => {
if(setErr) {
logger.warn(setErr);
} else {
logger.debug("redis: video information cache set for: " + videoID);
}
callback(false, data); // don't fail
});
} else {
callback(false, data); // don't fail
}
} else {
callback(ytErr, data)
}
});
} else {
logger.debug("redis: fetched video information from cache: " + videoID);
callback(getErr, JSON.parse(result));
}
});
};
}
module.exports = exportObject;

69
src/utils/youtubeApi.ts Normal file
View File

@@ -0,0 +1,69 @@
import {config} from '../config';
import {Logger} from './logger';
import * as redis from './redis';
// @ts-ignore
import YouTubeAPI from 'youtube-api';
import {YouTubeAPI as youtubeApiTest} from '../../test/youtubeMock';
let _youtubeApi: {
listVideos: (videoID: string, callback: (err: string | boolean, data: any) => void) => void
};
// If in test mode, return a mocked youtube object
// otherwise return an authenticated youtube api
if (config.mode === "test") {
_youtubeApi = youtubeApiTest;
}
else {
_youtubeApi = YouTubeAPI;
YouTubeAPI.authenticate({
type: "key",
key: config.youtubeAPIKey,
});
// YouTubeAPI.videos.list wrapper with cacheing
_youtubeApi.listVideos = (videoID: string, callback: (err: string | boolean, data: any) => void) => {
const part = 'contentDetails,snippet';
if (videoID.length !== 11 || videoID.includes(".")) {
callback("Invalid video ID", undefined);
return;
}
const redisKey = "youtube.video." + videoID;
redis.get(redisKey, (getErr: string, result: string) => {
if (getErr || !result) {
Logger.debug("redis: no cache for video information: " + videoID);
YouTubeAPI.videos.list({
part,
id: videoID,
}, (ytErr: boolean | string, data: any) => {
if (!ytErr) {
// Only set cache if data returned
if (data.items.length > 0) {
redis.set(redisKey, JSON.stringify(data), (setErr: string) => {
if (setErr) {
Logger.warn(setErr);
} else {
Logger.debug("redis: video information cache set for: " + videoID);
}
callback(false, data); // don't fail
});
} else {
callback(false, data); // don't fail
}
} else {
callback(ytErr, data);
}
});
} else {
Logger.debug("redis: fetched video information from cache: " + videoID);
callback(getErr, JSON.parse(result));
}
});
};
}
export {
_youtubeApi as YouTubeAPI
}