mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-06 11:36:58 +03:00
fix non-format eslint in src/
This commit is contained in:
51
.eslintrc.js
51
.eslintrc.js
@@ -1,24 +1,29 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: false,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
// TODO: Remove warn rules when not needed anymore
|
||||
"no-self-assign": "off",
|
||||
"semi": "warn",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
};
|
||||
env: {
|
||||
browser: false,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
// TODO: Remove warn rules when not needed anymore
|
||||
"no-self-assign": "off",
|
||||
"semi": "warn",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-trailing-spaces": "warn",
|
||||
"prefer-template": "warn",
|
||||
"quotes": ["warn", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"no-multiple-empty-lines": ["error", { max: 2, maxEOF: 0 }],
|
||||
"indent": ["warn", 4],
|
||||
},
|
||||
};
|
||||
|
||||
162
src/app.ts
162
src/app.ts
@@ -1,42 +1,42 @@
|
||||
import express, {Request, RequestHandler, Response, Router} from 'express';
|
||||
import {config} from './config';
|
||||
import {oldSubmitSponsorTimes} from './routes/oldSubmitSponsorTimes';
|
||||
import {oldGetVideoSponsorTimes} from './routes/oldGetVideoSponsorTimes';
|
||||
import {postSegmentShift} from './routes/postSegmentShift';
|
||||
import {postWarning} from './routes/postWarning';
|
||||
import {getIsUserVIP} from './routes/getIsUserVIP';
|
||||
import {deleteLockCategoriesEndpoint} from './routes/deleteLockCategories';
|
||||
import {postLockCategories} from './routes/postLockCategories';
|
||||
import {getUserInfo} from './routes/getUserInfo';
|
||||
import {getDaysSavedFormatted} from './routes/getDaysSavedFormatted';
|
||||
import {getTotalStats} from './routes/getTotalStats';
|
||||
import {getTopUsers} from './routes/getTopUsers';
|
||||
import {getViewsForUser} from './routes/getViewsForUser';
|
||||
import {getSavedTimeForUser} from './routes/getSavedTimeForUser';
|
||||
import {addUserAsVIP} from './routes/addUserAsVIP';
|
||||
import {shadowBanUser} from './routes/shadowBanUser';
|
||||
import {getUsername} from './routes/getUsername';
|
||||
import {setUsername} from './routes/setUsername';
|
||||
import {viewedVideoSponsorTime} from './routes/viewedVideoSponsorTime';
|
||||
import {voteOnSponsorTime, getUserID as voteGetUserID} from './routes/voteOnSponsorTime';
|
||||
import {getSkipSegmentsByHash} from './routes/getSkipSegmentsByHash';
|
||||
import {postSkipSegments} from './routes/postSkipSegments';
|
||||
import {endpoint as getSkipSegments} from './routes/getSkipSegments';
|
||||
import {userCounter} from './middleware/userCounter';
|
||||
import {loggerMiddleware} from './middleware/logger';
|
||||
import {corsMiddleware} from './middleware/cors';
|
||||
import {apiCspMiddleware} from './middleware/apiCsp';
|
||||
import {rateLimitMiddleware} from './middleware/requestRateLimit';
|
||||
import dumpDatabase, {redirectLink} from './routes/dumpDatabase';
|
||||
import {endpoint as getSegmentInfo} from './routes/getSegmentInfo';
|
||||
import {postClearCache} from './routes/postClearCache';
|
||||
import { addUnlistedVideo } from './routes/addUnlistedVideo';
|
||||
import {postPurgeAllSegments} from './routes/postPurgeAllSegments';
|
||||
import {getUserID} from './routes/getUserID';
|
||||
import {getLockCategories} from './routes/getLockCategories';
|
||||
import {getLockCategoriesByHash} from './routes/getLockCategoriesByHash';
|
||||
import ExpressPromiseRouter from 'express-promise-router';
|
||||
import { Server } from 'http';
|
||||
import express, {Request, RequestHandler, Response, Router} from "express";
|
||||
import {config} from "./config";
|
||||
import {oldSubmitSponsorTimes} from "./routes/oldSubmitSponsorTimes";
|
||||
import {oldGetVideoSponsorTimes} from "./routes/oldGetVideoSponsorTimes";
|
||||
import {postSegmentShift} from "./routes/postSegmentShift";
|
||||
import {postWarning} from "./routes/postWarning";
|
||||
import {getIsUserVIP} from "./routes/getIsUserVIP";
|
||||
import {deleteLockCategoriesEndpoint} from "./routes/deleteLockCategories";
|
||||
import {postLockCategories} from "./routes/postLockCategories";
|
||||
import {getUserInfo} from "./routes/getUserInfo";
|
||||
import {getDaysSavedFormatted} from "./routes/getDaysSavedFormatted";
|
||||
import {getTotalStats} from "./routes/getTotalStats";
|
||||
import {getTopUsers} from "./routes/getTopUsers";
|
||||
import {getViewsForUser} from "./routes/getViewsForUser";
|
||||
import {getSavedTimeForUser} from "./routes/getSavedTimeForUser";
|
||||
import {addUserAsVIP} from "./routes/addUserAsVIP";
|
||||
import {shadowBanUser} from "./routes/shadowBanUser";
|
||||
import {getUsername} from "./routes/getUsername";
|
||||
import {setUsername} from "./routes/setUsername";
|
||||
import {viewedVideoSponsorTime} from "./routes/viewedVideoSponsorTime";
|
||||
import {voteOnSponsorTime, getUserID as voteGetUserID} from "./routes/voteOnSponsorTime";
|
||||
import {getSkipSegmentsByHash} from "./routes/getSkipSegmentsByHash";
|
||||
import {postSkipSegments} from "./routes/postSkipSegments";
|
||||
import {endpoint as getSkipSegments} from "./routes/getSkipSegments";
|
||||
import {userCounter} from "./middleware/userCounter";
|
||||
import {loggerMiddleware} from "./middleware/logger";
|
||||
import {corsMiddleware} from "./middleware/cors";
|
||||
import {apiCspMiddleware} from "./middleware/apiCsp";
|
||||
import {rateLimitMiddleware} from "./middleware/requestRateLimit";
|
||||
import dumpDatabase, {redirectLink} from "./routes/dumpDatabase";
|
||||
import {endpoint as getSegmentInfo} from "./routes/getSegmentInfo";
|
||||
import {postClearCache} from "./routes/postClearCache";
|
||||
import { addUnlistedVideo } from "./routes/addUnlistedVideo";
|
||||
import {postPurgeAllSegments} from "./routes/postPurgeAllSegments";
|
||||
import {getUserID} from "./routes/getUserID";
|
||||
import {getLockCategories} from "./routes/getLockCategories";
|
||||
import {getLockCategoriesByHash} from "./routes/getLockCategoriesByHash";
|
||||
import ExpressPromiseRouter from "express-promise-router";
|
||||
import { Server } from "http";
|
||||
|
||||
export function createServer(callback: () => void): Server {
|
||||
// Create a service (the app object is just a callback).
|
||||
@@ -54,10 +54,10 @@ export function createServer(callback: () => void): Server {
|
||||
if (config.userCounterURL) app.use(userCounter);
|
||||
|
||||
// Setup pretty JSON
|
||||
if (config.mode === "development") app.set('json spaces', 2);
|
||||
if (config.mode === "development") app.set("json spaces", 2);
|
||||
|
||||
// Set production mode
|
||||
app.set('env', config.mode || 'production');
|
||||
app.set("env", config.mode || "production");
|
||||
|
||||
setupRoutes(router);
|
||||
|
||||
@@ -74,102 +74,102 @@ function setupRoutes(router: Router) {
|
||||
}
|
||||
|
||||
//add the get function
|
||||
router.get('/api/getVideoSponsorTimes', oldGetVideoSponsorTimes);
|
||||
router.get("/api/getVideoSponsorTimes", oldGetVideoSponsorTimes);
|
||||
|
||||
//add the oldpost function
|
||||
router.get('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
|
||||
router.post('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
|
||||
router.get("/api/postVideoSponsorTimes", oldSubmitSponsorTimes);
|
||||
router.post("/api/postVideoSponsorTimes", oldSubmitSponsorTimes);
|
||||
|
||||
//add the skip segments functions
|
||||
router.get('/api/skipSegments', getSkipSegments);
|
||||
router.post('/api/skipSegments', postSkipSegments);
|
||||
router.get("/api/skipSegments", getSkipSegments);
|
||||
router.post("/api/skipSegments", postSkipSegments);
|
||||
|
||||
// add the privacy protecting skip segments functions
|
||||
router.get('/api/skipSegments/:prefix', getSkipSegmentsByHash);
|
||||
router.get("/api/skipSegments/:prefix", getSkipSegmentsByHash);
|
||||
|
||||
//voting endpoint
|
||||
router.get('/api/voteOnSponsorTime', ...voteEndpoints);
|
||||
router.post('/api/voteOnSponsorTime', ...voteEndpoints);
|
||||
router.get("/api/voteOnSponsorTime", ...voteEndpoints);
|
||||
router.post("/api/voteOnSponsorTime", ...voteEndpoints);
|
||||
|
||||
//Endpoint when a submission is skipped
|
||||
router.get('/api/viewedVideoSponsorTime', ...viewEndpoints);
|
||||
router.post('/api/viewedVideoSponsorTime', ...viewEndpoints);
|
||||
router.get("/api/viewedVideoSponsorTime", ...viewEndpoints);
|
||||
router.post("/api/viewedVideoSponsorTime", ...viewEndpoints);
|
||||
|
||||
//To set your username for the stats view
|
||||
router.post('/api/setUsername', setUsername);
|
||||
router.post("/api/setUsername", setUsername);
|
||||
|
||||
//get what username this user has
|
||||
router.get('/api/getUsername', getUsername);
|
||||
router.get("/api/getUsername", getUsername);
|
||||
|
||||
//Endpoint used to hide a certain user's data
|
||||
router.post('/api/shadowBanUser', shadowBanUser);
|
||||
router.post("/api/shadowBanUser", shadowBanUser);
|
||||
|
||||
//Endpoint used to make a user a VIP user with special privileges
|
||||
router.post('/api/addUserAsVIP', addUserAsVIP);
|
||||
router.post("/api/addUserAsVIP", addUserAsVIP);
|
||||
|
||||
//Gets all the views added up for one userID
|
||||
//Useful to see how much one user has contributed
|
||||
router.get('/api/getViewsForUser', getViewsForUser);
|
||||
router.get("/api/getViewsForUser", getViewsForUser);
|
||||
|
||||
//Gets all the saved time added up (views * sponsor length) for one userID
|
||||
//Useful to see how much one user has contributed
|
||||
//In minutes
|
||||
router.get('/api/getSavedTimeForUser', getSavedTimeForUser);
|
||||
router.get("/api/getSavedTimeForUser", getSavedTimeForUser);
|
||||
|
||||
router.get('/api/getTopUsers', getTopUsers);
|
||||
router.get("/api/getTopUsers", getTopUsers);
|
||||
|
||||
//send out totals
|
||||
//send the total submissions, total views and total minutes saved
|
||||
router.get('/api/getTotalStats', getTotalStats);
|
||||
router.get("/api/getTotalStats", getTotalStats);
|
||||
|
||||
router.get('/api/getUserInfo', getUserInfo);
|
||||
router.get('/api/userInfo', getUserInfo);
|
||||
router.get("/api/getUserInfo", getUserInfo);
|
||||
router.get("/api/userInfo", getUserInfo);
|
||||
|
||||
//send out a formatted time saved total
|
||||
router.get('/api/getDaysSavedFormatted', getDaysSavedFormatted);
|
||||
router.get("/api/getDaysSavedFormatted", getDaysSavedFormatted);
|
||||
|
||||
//submit video to lock categories
|
||||
router.post('/api/noSegments', postLockCategories);
|
||||
router.post('/api/lockCategories', postLockCategories);
|
||||
router.post("/api/noSegments", postLockCategories);
|
||||
router.post("/api/lockCategories", postLockCategories);
|
||||
|
||||
router.delete('/api/noSegments', deleteLockCategoriesEndpoint);
|
||||
router.delete('/api/lockCategories', deleteLockCategoriesEndpoint);
|
||||
router.delete("/api/noSegments", deleteLockCategoriesEndpoint);
|
||||
router.delete("/api/lockCategories", deleteLockCategoriesEndpoint);
|
||||
|
||||
//get if user is a vip
|
||||
router.get('/api/isUserVIP', getIsUserVIP);
|
||||
router.get("/api/isUserVIP", getIsUserVIP);
|
||||
|
||||
//sent user a warning
|
||||
router.post('/api/warnUser', postWarning);
|
||||
router.post("/api/warnUser", postWarning);
|
||||
|
||||
//get if user is a vip
|
||||
router.post('/api/segmentShift', postSegmentShift);
|
||||
router.post("/api/segmentShift", postSegmentShift);
|
||||
|
||||
//get segment info
|
||||
router.get('/api/segmentInfo', getSegmentInfo);
|
||||
router.get("/api/segmentInfo", getSegmentInfo);
|
||||
|
||||
//clear cache as VIP
|
||||
router.post('/api/clearCache', postClearCache);
|
||||
router.post("/api/clearCache", postClearCache);
|
||||
|
||||
//purge all segments for VIP
|
||||
router.post('/api/purgeAllSegments', postPurgeAllSegments);
|
||||
router.post("/api/purgeAllSegments", postPurgeAllSegments);
|
||||
|
||||
router.post("/api/unlistedVideo", addUnlistedVideo);
|
||||
|
||||
router.post('/api/unlistedVideo', addUnlistedVideo);
|
||||
|
||||
// get userID from username
|
||||
router.get('/api/userID', getUserID);
|
||||
router.get("/api/userID", getUserID);
|
||||
|
||||
// get lock categores from userID
|
||||
router.get('/api/lockCategories', getLockCategories);
|
||||
router.get("/api/lockCategories", getLockCategories);
|
||||
|
||||
// get privacy protecting lock categories functions
|
||||
router.get('/api/lockCategories/:prefix', getLockCategoriesByHash);
|
||||
router.get("/api/lockCategories/:prefix", getLockCategoriesByHash);
|
||||
|
||||
if (config.postgres) {
|
||||
router.get('/database', (req, res) => dumpDatabase(req, res, true));
|
||||
router.get('/database.json', (req, res) => dumpDatabase(req, res, false));
|
||||
router.get('/database/*', redirectLink);
|
||||
router.get("/database", (req, res) => dumpDatabase(req, res, true));
|
||||
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
|
||||
router.get("/database/*", redirectLink);
|
||||
} else {
|
||||
router.get('/database.db', function (req: Request, res: Response) {
|
||||
router.get("/database.db", function (req: Request, res: Response) {
|
||||
res.sendFile("./databases/sponsorTimes.db", {root: "./"});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import fs from 'fs';
|
||||
import fs from "fs";
|
||||
import {SBSConfig} from "./types/config.model";
|
||||
import packageJson from "../package.json";
|
||||
|
||||
const isTestMode = process.env.npm_lifecycle_script === packageJson.scripts.test;
|
||||
const configFile = process.env.TEST_POSTGRES ? 'ci.json'
|
||||
: isTestMode ? 'test.json'
|
||||
: 'config.json';
|
||||
export const config: SBSConfig = JSON.parse(fs.readFileSync(configFile).toString('utf8'));
|
||||
const configFile = process.env.TEST_POSTGRES ? "ci.json"
|
||||
: isTestMode ? "test.json"
|
||||
: "config.json";
|
||||
export const config: SBSConfig = JSON.parse(fs.readFileSync(configFile).toString("utf8"));
|
||||
|
||||
addDefaults(config, {
|
||||
port: 80,
|
||||
@@ -54,8 +54,8 @@ addDefaults(config, {
|
||||
dumpDatabase: {
|
||||
enabled: false,
|
||||
minTimeBetweenMs: 60000,
|
||||
appExportPath: './docker/database-export',
|
||||
postgresExportPath: '/opt/exports',
|
||||
appExportPath: "./docker/database-export",
|
||||
postgresExportPath: "/opt/exports",
|
||||
tables: [{
|
||||
name: "sponsorTimes",
|
||||
order: "timeSubmitted"
|
||||
|
||||
@@ -8,56 +8,56 @@ import { DBSegment } from "../types/segments.model";
|
||||
const jobConfig = serverConfig?.crons?.downvoteSegmentArchive;
|
||||
|
||||
export const archiveDownvoteSegment = async (dayLimit: number, voteLimit: number, runTime?: number): Promise<number> => {
|
||||
const timeNow = runTime || new Date().getTime();
|
||||
const threshold = dayLimit * 86400000;
|
||||
const timeNow = runTime || new Date().getTime();
|
||||
const threshold = dayLimit * 86400000;
|
||||
|
||||
Logger.info(`DownvoteSegmentArchiveJob starts at ${timeNow}`);
|
||||
try {
|
||||
Logger.info(`DownvoteSegmentArchiveJob starts at ${timeNow}`);
|
||||
try {
|
||||
// insert into archive sponsorTime
|
||||
await db.prepare(
|
||||
'run',
|
||||
`INSERT INTO "archivedSponsorTimes"
|
||||
"run",
|
||||
`INSERT INTO "archivedSponsorTimes"
|
||||
SELECT *
|
||||
FROM "sponsorTimes"
|
||||
WHERE "votes" < ? AND (? - "timeSubmitted") > ?`,
|
||||
[
|
||||
voteLimit,
|
||||
timeNow,
|
||||
threshold
|
||||
]
|
||||
[
|
||||
voteLimit,
|
||||
timeNow,
|
||||
threshold
|
||||
]
|
||||
) as DBSegment[];
|
||||
|
||||
} catch (err) {
|
||||
Logger.error('Execption when insert segment in archivedSponsorTimes');
|
||||
Logger.error(err);
|
||||
return 1;
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error("Execption when insert segment in archivedSponsorTimes");
|
||||
Logger.error(err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// remove from sponsorTime
|
||||
try {
|
||||
// remove from sponsorTime
|
||||
try {
|
||||
await db.prepare(
|
||||
'run',
|
||||
'DELETE FROM "sponsorTimes" WHERE "votes" < ? AND (? - "timeSubmitted") > ?',
|
||||
[
|
||||
voteLimit,
|
||||
timeNow,
|
||||
threshold
|
||||
]
|
||||
"run",
|
||||
'DELETE FROM "sponsorTimes" WHERE "votes" < ? AND (? - "timeSubmitted") > ?',
|
||||
[
|
||||
voteLimit,
|
||||
timeNow,
|
||||
threshold
|
||||
]
|
||||
) as DBSegment[];
|
||||
|
||||
} catch (err) {
|
||||
Logger.error('Execption when deleting segment in sponsorTimes');
|
||||
Logger.error(err);
|
||||
return 1;
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error("Execption when deleting segment in sponsorTimes");
|
||||
Logger.error(err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Logger.info('DownvoteSegmentArchiveJob finished');
|
||||
return 0;
|
||||
Logger.info("DownvoteSegmentArchiveJob finished");
|
||||
return 0;
|
||||
};
|
||||
|
||||
const DownvoteSegmentArchiveJob = new CronJob(
|
||||
jobConfig?.schedule || new Date(1),
|
||||
() => archiveDownvoteSegment(jobConfig?.timeThresholdInDays, jobConfig?.voteThreshold)
|
||||
jobConfig?.schedule || new Date(1),
|
||||
() => archiveDownvoteSegment(jobConfig?.timeThresholdInDays, jobConfig?.voteThreshold)
|
||||
);
|
||||
|
||||
export default DownvoteSegmentArchiveJob;
|
||||
|
||||
@@ -3,11 +3,11 @@ import { config } from "../config";
|
||||
import DownvoteSegmentArchiveJob from "./downvoteSegmentArchiveJob";
|
||||
|
||||
export function startAllCrons (): void {
|
||||
if (config?.crons?.enabled) {
|
||||
Logger.info("Crons started");
|
||||
if (config?.crons?.enabled) {
|
||||
Logger.info("Crons started");
|
||||
|
||||
DownvoteSegmentArchiveJob.start();
|
||||
} else {
|
||||
Logger.info("Crons dissabled");
|
||||
}
|
||||
DownvoteSegmentArchiveJob.start();
|
||||
} else {
|
||||
Logger.info("Crons dissabled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ export interface IDatabase {
|
||||
prepare(type: QueryType, query: string, params?: any[]): Promise<any | any[] | void>;
|
||||
}
|
||||
|
||||
export type QueryType = 'get' | 'all' | 'run';
|
||||
export type QueryType = "get" | "all" | "run";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Logger} from '../utils/logger';
|
||||
import {IDatabase, QueryType} from './IDatabase';
|
||||
import {Logger} from "../utils/logger";
|
||||
import {IDatabase, QueryType} from "./IDatabase";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import MysqlInterface from 'sync-mysql';
|
||||
import MysqlInterface from "sync-mysql";
|
||||
|
||||
export class Mysql implements IDatabase {
|
||||
private connection: any;
|
||||
@@ -19,15 +19,15 @@ export class Mysql implements IDatabase {
|
||||
const queryResult = this.connection.query(query, params);
|
||||
|
||||
switch (type) {
|
||||
case 'get': {
|
||||
return queryResult[0];
|
||||
}
|
||||
case 'all': {
|
||||
return queryResult;
|
||||
}
|
||||
case 'run': {
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
return queryResult[0];
|
||||
}
|
||||
case "all": {
|
||||
return queryResult;
|
||||
}
|
||||
case "run": {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Logger } from '../utils/logger';
|
||||
import { IDatabase, QueryType } from './IDatabase';
|
||||
import { Client, Pool, types } from 'pg';
|
||||
import { Logger } from "../utils/logger";
|
||||
import { IDatabase, QueryType } from "./IDatabase";
|
||||
import { Client, Pool, types } from "pg";
|
||||
|
||||
import fs from "fs";
|
||||
|
||||
@@ -11,7 +11,7 @@ types.setTypeParser(1700, function(val) {
|
||||
|
||||
// return int8 (pg_type oid=20) as int
|
||||
types.setTypeParser(20, function(val) {
|
||||
return parseInt(val, 10);
|
||||
return parseInt(val, 10);
|
||||
});
|
||||
|
||||
export class Postgres implements IDatabase {
|
||||
@@ -47,8 +47,8 @@ export class Postgres implements IDatabase {
|
||||
// Convert query to use numbered parameters
|
||||
let count = 1;
|
||||
for (let char = 0; char < query.length; char++) {
|
||||
if (query.charAt(char) === '?') {
|
||||
query = query.slice(0, char) + "$" + count + query.slice(char + 1);
|
||||
if (query.charAt(char) === "?") {
|
||||
query = `${query.slice(0, char)}$${count}${query.slice(char + 1)}`;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -58,19 +58,19 @@ export class Postgres implements IDatabase {
|
||||
const queryResult = await this.pool.query({text: query, values: params});
|
||||
|
||||
switch (type) {
|
||||
case 'get': {
|
||||
const value = queryResult.rows[0];
|
||||
Logger.debug(`result (postgres): ${JSON.stringify(value)}`);
|
||||
return value;
|
||||
}
|
||||
case 'all': {
|
||||
const values = queryResult.rows;
|
||||
Logger.debug(`result (postgres): ${values}`);
|
||||
return values;
|
||||
}
|
||||
case 'run': {
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const value = queryResult.rows[0];
|
||||
Logger.debug(`result (postgres): ${JSON.stringify(value)}`);
|
||||
return value;
|
||||
}
|
||||
case "all": {
|
||||
const values = queryResult.rows;
|
||||
Logger.debug(`result (postgres): ${values}`);
|
||||
return values;
|
||||
}
|
||||
case "run": {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export class Postgres implements IDatabase {
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
|
||||
if ((await client.query(`SELECT * FROM pg_database WHERE datname = '${this.config.postgres.database}'`)).rowCount == 0) {
|
||||
await client.query(`CREATE DATABASE "${this.config.postgres.database}"
|
||||
WITH
|
||||
@@ -101,25 +101,25 @@ export class Postgres implements IDatabase {
|
||||
const versionCodeInfo = await this.pool.query("SELECT value FROM config WHERE key = 'version'");
|
||||
let versionCode = versionCodeInfo.rows[0] ? versionCodeInfo.rows[0].value : 0;
|
||||
|
||||
let path = schemaFolder + "/_upgrade_" + fileNamePrefix + "_" + (parseInt(versionCode) + 1) + ".sql";
|
||||
Logger.debug('db update: trying ' + path);
|
||||
let path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
while (fs.existsSync(path)) {
|
||||
Logger.debug('db update: updating ' + path);
|
||||
Logger.debug(`db update: updating ${path}`);
|
||||
await this.pool.query(this.processUpgradeQuery(fs.readFileSync(path).toString()));
|
||||
|
||||
versionCode = (await this.pool.query("SELECT value FROM config WHERE key = 'version'"))?.rows[0]?.value;
|
||||
path = schemaFolder + "/_upgrade_" + fileNamePrefix + "_" + (parseInt(versionCode) + 1) + ".sql";
|
||||
Logger.debug('db update: trying ' + path);
|
||||
path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
}
|
||||
Logger.debug('db update: no file ' + path);
|
||||
Logger.debug(`db update: no file ${path}`);
|
||||
}
|
||||
|
||||
private async applyIndexes(fileNamePrefix: string, schemaFolder: string) {
|
||||
const path = schemaFolder + "/_" + fileNamePrefix + "_indexes.sql";
|
||||
const path = `${schemaFolder}/_${fileNamePrefix}_indexes.sql`;
|
||||
if (fs.existsSync(path)) {
|
||||
await this.pool.query(fs.readFileSync(path).toString());
|
||||
} else {
|
||||
Logger.debug('failed to apply indexes to ' + fileNamePrefix);
|
||||
Logger.debug(`failed to apply indexes to ${fileNamePrefix}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {IDatabase, QueryType} from './IDatabase';
|
||||
import Sqlite3, {Database, Database as SQLiteDatabase} from 'better-sqlite3';
|
||||
import {IDatabase, QueryType} from "./IDatabase";
|
||||
import Sqlite3, {Database, Database as SQLiteDatabase} from "better-sqlite3";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {Logger} from "../utils/logger";
|
||||
|
||||
export class Sqlite implements IDatabase {
|
||||
private db: SQLiteDatabase;
|
||||
@@ -17,16 +17,16 @@ export class Sqlite implements IDatabase {
|
||||
const preparedQuery = this.db.prepare(query);
|
||||
|
||||
switch (type) {
|
||||
case 'get': {
|
||||
return preparedQuery.get(...params);
|
||||
}
|
||||
case 'all': {
|
||||
return preparedQuery.all(...params);
|
||||
}
|
||||
case 'run': {
|
||||
preparedQuery.run(...params);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
return preparedQuery.get(...params);
|
||||
}
|
||||
case "all": {
|
||||
return preparedQuery.all(...params);
|
||||
}
|
||||
case "run": {
|
||||
preparedQuery.run(...params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,17 +69,17 @@ export class Sqlite implements IDatabase {
|
||||
const versionCodeInfo = db.prepare("SELECT value FROM config WHERE key = ?").get("version");
|
||||
let versionCode = versionCodeInfo ? versionCodeInfo.value : 0;
|
||||
|
||||
let path = schemaFolder + "/_upgrade_" + fileNamePrefix + "_" + (parseInt(versionCode) + 1) + ".sql";
|
||||
Logger.debug('db update: trying ' + path);
|
||||
let path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
while (fs.existsSync(path)) {
|
||||
Logger.debug('db update: updating ' + path);
|
||||
Logger.debug(`db update: updating ${path}`);
|
||||
db.exec(this.processUpgradeQuery(fs.readFileSync(path).toString()));
|
||||
|
||||
versionCode = db.prepare("SELECT value FROM config WHERE key = ?").get("version").value;
|
||||
path = schemaFolder + "/_upgrade_" + fileNamePrefix + "_" + (parseInt(versionCode) + 1) + ".sql";
|
||||
Logger.debug('db update: trying ' + path);
|
||||
path = `${schemaFolder}/_upgrade_${fileNamePrefix}_${(parseInt(versionCode) + 1)}.sql`;
|
||||
Logger.debug(`db update: trying ${path}`);
|
||||
}
|
||||
Logger.debug('db update: no file ' + path);
|
||||
Logger.debug(`db update: no file ${path}`);
|
||||
}
|
||||
|
||||
private static processUpgradeQuery(query: string): string {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {config} from '../config';
|
||||
import {Sqlite} from './Sqlite';
|
||||
import {Mysql} from './Mysql';
|
||||
import {Postgres} from './Postgres';
|
||||
import {IDatabase} from './IDatabase';
|
||||
import {config} from "../config";
|
||||
import {Sqlite} from "./Sqlite";
|
||||
import {Mysql} from "./Mysql";
|
||||
import {Postgres} from "./Postgres";
|
||||
import {IDatabase} from "./IDatabase";
|
||||
|
||||
|
||||
let db: IDatabase;
|
||||
@@ -14,7 +14,7 @@ if (config.mysql) {
|
||||
db = new Postgres({
|
||||
dbSchemaFileName: config.dbSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: 'sponsorTimes',
|
||||
fileNamePrefix: "sponsorTimes",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
postgres: {
|
||||
@@ -29,7 +29,7 @@ if (config.mysql) {
|
||||
privateDB = new Postgres({
|
||||
dbSchemaFileName: config.privateDBSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: 'private',
|
||||
fileNamePrefix: "private",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
postgres: {
|
||||
@@ -45,7 +45,7 @@ if (config.mysql) {
|
||||
dbPath: config.db,
|
||||
dbSchemaFileName: config.dbSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: 'sponsorTimes',
|
||||
fileNamePrefix: "sponsorTimes",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
enableWalCheckpointNumber: !config.readOnly && config.mode === "production"
|
||||
@@ -54,7 +54,7 @@ if (config.mysql) {
|
||||
dbPath: config.privateDB,
|
||||
dbSchemaFileName: config.privateDBSchema,
|
||||
dbSchemaFolder: config.schemaFolder,
|
||||
fileNamePrefix: 'private',
|
||||
fileNamePrefix: "private",
|
||||
readOnly: config.readOnly,
|
||||
createDbIfNotExists: config.createDatabaseIfNotExist,
|
||||
enableWalCheckpointNumber: false
|
||||
@@ -66,7 +66,7 @@ async function initDb(): Promise<void> {
|
||||
|
||||
if (db instanceof Sqlite) {
|
||||
// Attach private db to main db
|
||||
(db as Sqlite).attachDatabase(config.privateDB, 'privateDB');
|
||||
(db as Sqlite).attachDatabase(config.privateDB, "privateDB");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {config} from "./config";
|
||||
import {initDb} from './databases/databases';
|
||||
import {initDb} from "./databases/databases";
|
||||
import {createServer} from "./app";
|
||||
import {Logger} from "./utils/logger";
|
||||
import {startAllCrons} from "./cronjob";
|
||||
@@ -8,7 +8,7 @@ async function init() {
|
||||
await initDb();
|
||||
|
||||
createServer(() => {
|
||||
Logger.info("Server started on port " + config.port + ".");
|
||||
Logger.info(`Server started on port ${config.port}.`);
|
||||
|
||||
// ignite cron job after server created
|
||||
startAllCrons();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
|
||||
export function apiCspMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
res.header("Content-Security-Policy", "script-src 'none'; object-src 'none'");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
|
||||
export function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Logger} from '../utils/logger';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {Logger} from "../utils/logger";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
|
||||
export function loggerMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
Logger.info(`Request received: ${req.method} ${req.url}`);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {getIP} from '../utils/getIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import {RateLimitConfig} from '../types/config.model';
|
||||
import {Request} from 'express';
|
||||
import { isUserVIP } from '../utils/isUserVIP';
|
||||
import { UserID } from '../types/user.model';
|
||||
import {getIP} from "../utils/getIP";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import {RateLimitConfig} from "../types/config.model";
|
||||
import {Request} from "express";
|
||||
import { isUserVIP } from "../utils/isUserVIP";
|
||||
import { UserID } from "../types/user.model";
|
||||
|
||||
export function rateLimitMiddleware(limitConfig: RateLimitConfig, getUserID?: (req: Request) => UserID): rateLimit.RateLimit {
|
||||
return rateLimit({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import fetch from 'node-fetch';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {config} from '../config';
|
||||
import {getIP} from '../utils/getIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import fetch from "node-fetch";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {config} from "../config";
|
||||
import {getIP} from "../utils/getIP";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
|
||||
export function userCounter(req: Request, res: Response, next: NextFunction): void {
|
||||
fetch(config.userCounterURL + "/api/v1/addIP?hashedIP=" + getHash(getIP(req), 1), {method: "POST"})
|
||||
.catch(() => Logger.debug("Failing to connect to user counter at: " + config.userCounterURL));
|
||||
fetch(`${config.userCounterURL}/api/v1/addIP?hashedIP=${getHash(getIP(req), 1)}`, {method: "POST"})
|
||||
.catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Request, Response} from 'express';
|
||||
import { db } from '../databases/databases';
|
||||
import { Logger } from '../utils/logger';
|
||||
import {Request, Response} from "express";
|
||||
import { db } from "../databases/databases";
|
||||
import { Logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Optional API method that will be used temporarily to help collect
|
||||
* unlisted videos created before 2017
|
||||
*
|
||||
*
|
||||
* https://support.google.com/youtube/answer/9230970
|
||||
*/
|
||||
|
||||
@@ -21,7 +21,7 @@ export function addUnlistedVideo(req: Request, res: Response): Response {
|
||||
|
||||
try {
|
||||
const timeSubmitted = Date.now();
|
||||
db.prepare('run', `INSERT INTO "unlistedVideos" ("videoID", "year", "views", "channelID", "timeSubmitted") values (?, ?, ?, ?, ?)`, [videoID, year, views, channelID, timeSubmitted]);
|
||||
db.prepare("run", `INSERT INTO "unlistedVideos" ("videoID", "year", "views", "channelID", "timeSubmitted") values (?, ?, ?, ?, ?)`, [videoID, year, views, channelID, timeSubmitted]);
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
return res.sendStatus(500);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {db} from '../databases/databases';
|
||||
import {config} from '../config';
|
||||
import {Request, Response} from 'express';
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {db} from "../databases/databases";
|
||||
import {config} from "../config";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
export async function addUserAsVIP(req: Request, res: Response): Promise<Response> {
|
||||
const userID = req.query.userID as string;
|
||||
@@ -9,7 +9,7 @@ export async function addUserAsVIP(req: Request, res: Response): Promise<Respons
|
||||
|
||||
const enabled = req.query.enabled === undefined
|
||||
? false
|
||||
: req.query.enabled === 'true';
|
||||
: req.query.enabled === "true";
|
||||
|
||||
if (userID == undefined || adminUserIDInput == undefined) {
|
||||
//invalid request
|
||||
@@ -25,14 +25,14 @@ export async function addUserAsVIP(req: Request, res: Response): Promise<Respons
|
||||
}
|
||||
|
||||
//check to see if this user is already a vip
|
||||
const row = await db.prepare('get', 'SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?', [userID]);
|
||||
const row = await db.prepare("get", 'SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?', [userID]);
|
||||
|
||||
if (enabled && row.userCount == 0) {
|
||||
//add them to the vip list
|
||||
await db.prepare('run', 'INSERT INTO "vipUsers" VALUES(?)', [userID]);
|
||||
await db.prepare("run", 'INSERT INTO "vipUsers" VALUES(?)', [userID]);
|
||||
} else if (!enabled && row.userCount > 0) {
|
||||
//remove them from the shadow ban list
|
||||
await db.prepare('run', 'DELETE FROM "vipUsers" WHERE "userID" = ?', [userID]);
|
||||
await db.prepare("run", 'DELETE FROM "vipUsers" WHERE "userID" = ?', [userID]);
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Request, Response} from 'express';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {db} from '../databases/databases';
|
||||
import { Category, VideoID } from '../types/segments.model';
|
||||
import { UserID } from '../types/user.model';
|
||||
import {Request, Response} from "express";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {db} from "../databases/databases";
|
||||
import { Category, VideoID } from "../types/segments.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
|
||||
export async function deleteLockCategoriesEndpoint(req: Request, res: Response): Promise<Response> {
|
||||
// Collect user input data
|
||||
@@ -19,7 +19,7 @@ export async function deleteLockCategoriesEndpoint(req: Request, res: Response):
|
||||
|| categories.length === 0
|
||||
) {
|
||||
return res.status(400).json({
|
||||
message: 'Bad Format',
|
||||
message: "Bad Format",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,18 +29,18 @@ export async function deleteLockCategoriesEndpoint(req: Request, res: Response):
|
||||
|
||||
if (!userIsVIP) {
|
||||
return res.status(403).json({
|
||||
message: 'Must be a VIP to mark videos.',
|
||||
message: "Must be a VIP to mark videos.",
|
||||
});
|
||||
}
|
||||
|
||||
await deleteLockCategories(videoID, categories);
|
||||
await deleteLockCategories(videoID, categories);
|
||||
|
||||
return res.status(200).json({message: 'Removed lock categories entrys for video ' + videoID});
|
||||
return res.status(200).json({message: `Removed lock categories entrys for video ${videoID}`});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param videoID
|
||||
*
|
||||
* @param videoID
|
||||
* @param categories If null, will remove all
|
||||
*/
|
||||
export async function deleteLockCategories(videoID: VideoID, categories: Category[]): Promise<void> {
|
||||
@@ -49,6 +49,6 @@ export async function deleteLockCategories(videoID: VideoID, categories: Categor
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
await db.prepare('run', 'DELETE FROM "lockCategories" WHERE "videoID" = ? AND "category" = ?', [videoID, entry.category]);
|
||||
await db.prepare("run", 'DELETE FROM "lockCategories" WHERE "videoID" = ? AND "category" = ?', [videoID, entry.category]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {Request, Response} from 'express';
|
||||
import { config } from '../config';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {db} from "../databases/databases";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {Request, Response} from "express";
|
||||
import { config } from "../config";
|
||||
import util from "util";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
const unlink = util.promisify(fs.unlink);
|
||||
|
||||
const ONE_MINUTE = 1000 * 60;
|
||||
@@ -31,8 +31,8 @@ const licenseHeader = `<p>The API and database follow <a href="https://creativec
|
||||
|
||||
const tables = config?.dumpDatabase?.tables ?? [];
|
||||
const MILLISECONDS_BETWEEN_DUMPS = config?.dumpDatabase?.minTimeBetweenMs ?? ONE_MINUTE;
|
||||
const appExportPath = config?.dumpDatabase?.appExportPath ?? './docker/database-export';
|
||||
const postgresExportPath = config?.dumpDatabase?.postgresExportPath ?? '/opt/exports';
|
||||
const appExportPath = config?.dumpDatabase?.appExportPath ?? "./docker/database-export";
|
||||
const postgresExportPath = config?.dumpDatabase?.postgresExportPath ?? "/opt/exports";
|
||||
const tableNames = tables.map(table => table.name);
|
||||
|
||||
interface TableDumpList {
|
||||
@@ -47,7 +47,7 @@ interface TableFile {
|
||||
}
|
||||
|
||||
if (tables.length === 0) {
|
||||
Logger.warn('[dumpDatabase] No tables configured');
|
||||
Logger.warn("[dumpDatabase] No tables configured");
|
||||
}
|
||||
|
||||
let lastUpdate = 0;
|
||||
@@ -71,7 +71,7 @@ function removeOutdatedDumps(exportPath: string): Promise<void> {
|
||||
files.forEach(file => {
|
||||
// we only care about files that start with "<tablename>_" and ends with .csv
|
||||
tableNames.forEach(tableName => {
|
||||
if (file.startsWith(`${tableName}`) && file.endsWith('.csv')) {
|
||||
if (file.startsWith(`${tableName}`) && file.endsWith(".csv")) {
|
||||
const filePath = path.join(exportPath, file);
|
||||
tableFiles[tableName].push({
|
||||
file: filePath,
|
||||
@@ -108,7 +108,7 @@ export default async function dumpDatabase(req: Request, res: Response, showPage
|
||||
updateQueueTime();
|
||||
|
||||
res.status(200);
|
||||
|
||||
|
||||
if (showPage) {
|
||||
res.send(`${styleHeader}
|
||||
<h1>SponsorBlock database dumps</h1>${licenseHeader}
|
||||
@@ -134,9 +134,9 @@ export default async function dumpDatabase(req: Request, res: Response, showPage
|
||||
<td><a href="/database/${item.tableName}.csv">${item.tableName}.csv</a></td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
${latestDumpFiles.length === 0 ? '<tr><td colspan="2">Please wait: Generating files</td></tr>' : ''}
|
||||
</tbody>
|
||||
}).join("")}
|
||||
${latestDumpFiles.length === 0 ? '<tr><td colspan="2">Please wait: Generating files</td></tr>' : ""}
|
||||
</tbody>
|
||||
</table>
|
||||
<hr/>
|
||||
${updateQueued ? `Update queued.` : ``} Last updated: ${lastUpdate ? new Date(lastUpdate).toUTCString() : `Unknown`}`);
|
||||
@@ -159,7 +159,7 @@ export default async function dumpDatabase(req: Request, res: Response, showPage
|
||||
}
|
||||
|
||||
async function getDbVersion(): Promise<number> {
|
||||
const row = await db.prepare('get', `SELECT "value" FROM "config" WHERE "key" = 'version'`);
|
||||
const row = await db.prepare("get", `SELECT "value" FROM "config" WHERE "key" = 'version'`);
|
||||
if (row === undefined) return 0;
|
||||
return row.value;
|
||||
}
|
||||
@@ -173,13 +173,13 @@ export async function redirectLink(req: Request, res: Response): Promise<void> {
|
||||
res.status(404).send("Not supported on this instance");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const file = latestDumpFiles.find((value) => `/database/${value.tableName}.csv` === req.path);
|
||||
|
||||
updateQueueTime();
|
||||
|
||||
if (file) {
|
||||
res.redirect("/download/" + file.fileName);
|
||||
res.redirect(`/download/${file.fileName}`);
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
@@ -198,13 +198,13 @@ async function queueDump(): Promise<void> {
|
||||
|
||||
try {
|
||||
await removeOutdatedDumps(appExportPath);
|
||||
|
||||
|
||||
const dumpFiles = [];
|
||||
|
||||
|
||||
for (const table of tables) {
|
||||
const fileName = `${table.name}_${startTime}.csv`;
|
||||
const file = `${postgresExportPath}/${fileName}`;
|
||||
await db.prepare('run', `COPY (SELECT * FROM "${table.name}"${table.order ? ` ORDER BY "${table.order}"` : ``})
|
||||
await db.prepare("run", `COPY (SELECT * FROM "${table.name}"${table.order ? ` ORDER BY "${table.order}"` : ``})
|
||||
TO '${file}' WITH (FORMAT CSV, HEADER true);`);
|
||||
dumpFiles.push({
|
||||
fileName,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from 'express';
|
||||
import {db} from "../databases/databases";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
export async function getDaysSavedFormatted(req: Request, res: Response): Promise<Response> {
|
||||
const row = await db.prepare('get', 'SELECT SUM(("endTime" - "startTime") / 60 / 60 / 24 * "views") as "daysSaved" from "sponsorTimes" where "shadowHidden" != 1', []);
|
||||
const row = await db.prepare("get", 'SELECT SUM(("endTime" - "startTime") / 60 / 60 / 24 * "views") as "daysSaved" from "sponsorTimes" where "shadowHidden" != 1', []);
|
||||
|
||||
if (row !== undefined) {
|
||||
//send this result
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Logger} from '../utils/logger';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {Request, Response} from 'express';
|
||||
import { HashedUserID, UserID } from '../types/user.model';
|
||||
import {Logger} from "../utils/logger";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {Request, Response} from "express";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
|
||||
export async function getIsUserVIP(req: Request, res: Response): Promise<Response> {
|
||||
const userID = req.query.userID as UserID;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {Request, Response} from 'express';
|
||||
import {db} from "../databases/databases";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {Request, Response} from "express";
|
||||
import { Category, VideoID } from "../types/segments.model";
|
||||
|
||||
export async function getLockCategories(req: Request, res: Response): Promise<Response> {
|
||||
@@ -13,7 +13,7 @@ export async function getLockCategories(req: Request, res: Response): Promise<Re
|
||||
|
||||
try {
|
||||
// Get existing lock categories markers
|
||||
const lockedCategories = await db.prepare('all', 'SELECT "category" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category}[];
|
||||
const lockedCategories = await db.prepare("all", 'SELECT "category" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category}[];
|
||||
if (lockedCategories.length === 0 || !lockedCategories[0]) return res.sendStatus(404);
|
||||
// map to array in JS becaues of SQL incompatibilities
|
||||
const categories = Object.values(lockedCategories).map((entry) => entry.category);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {Request, Response} from 'express';
|
||||
import {hashPrefixTester} from '../utils/hashPrefixTester';
|
||||
import {db} from "../databases/databases";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {Request, Response} from "express";
|
||||
import {hashPrefixTester} from "../utils/hashPrefixTester";
|
||||
import { Category, VideoID, VideoIDHash } from "../types/segments.model";
|
||||
|
||||
interface LockResultByHash {
|
||||
@@ -45,7 +45,7 @@ export async function getLockCategoriesByHash(req: Request, res: Response): Prom
|
||||
|
||||
try {
|
||||
// Get existing lock categories markers
|
||||
const lockedRows = await db.prepare('all', 'SELECT "videoID", "hashedVideoID" as "hash", "category" from "lockCategories" where "hashedVideoID" LIKE ?', [hashPrefix + '%']) as DBLock[];
|
||||
const lockedRows = await db.prepare("all", 'SELECT "videoID", "hashedVideoID" as "hash", "category" from "lockCategories" where "hashedVideoID" LIKE ?', [`${hashPrefix}%`]) as DBLock[];
|
||||
if (lockedRows.length === 0 || !lockedRows[0]) return res.sendStatus(404);
|
||||
// merge all locks
|
||||
return res.send(mergeLocks(lockedRows));
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from 'express';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {config} from '../config';
|
||||
import { Logger } from '../utils/logger';
|
||||
import {db} from "../databases/databases";
|
||||
import {Request, Response} from "express";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {config} from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
|
||||
const maxRewardTimePerSegmentInSeconds = config.maxRewardTimePerSegmentInSeconds ?? 86400;
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function getSavedTimeForUser(req: Request, res: Response): Promise<
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error("getSavedTimeForUser " + err);
|
||||
Logger.error(`getSavedTimeForUser ${err}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { db } from '../databases/databases';
|
||||
import { Request, Response } from "express";
|
||||
import { db } from "../databases/databases";
|
||||
import { DBSegment, SegmentUUID } from "../types/segments.model";
|
||||
|
||||
const isValidSegmentUUID = (str: string): boolean => /^([a-f0-9]{64}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/.test(str);
|
||||
|
||||
async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise<DBSegment> {
|
||||
try {
|
||||
return await db.prepare('get',
|
||||
return await db.prepare("get",
|
||||
`SELECT "videoID", "startTime", "endTime", "votes", "locked",
|
||||
"UUID", "userID", "timeSubmitted", "views", "category",
|
||||
"service", "videoDuration", "hidden", "reputation", "shadowHidden" FROM "sponsorTimes"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { config } from '../config';
|
||||
import { db, privateDB } from '../databases/databases';
|
||||
import { skipSegmentsHashKey, skipSegmentsKey } from '../utils/redisKeys';
|
||||
import { SBRecord } from '../types/lib.model';
|
||||
import { Request, Response } from "express";
|
||||
import { config } from "../config";
|
||||
import { db, privateDB } from "../databases/databases";
|
||||
import { skipSegmentsHashKey, skipSegmentsKey } from "../utils/redisKeys";
|
||||
import { SBRecord } from "../types/lib.model";
|
||||
import { ActionType, Category, CategoryActionType, DBSegment, HashedIP, IPAddress, OverlappingSegmentGroup, Segment, SegmentCache, SegmentUUID, Service, VideoData, VideoID, VideoIDHash, Visibility, VotableObject } from "../types/segments.model";
|
||||
import { getCategoryActionType } from '../utils/categoryInfo';
|
||||
import { getHash } from '../utils/getHash';
|
||||
import { getIP } from '../utils/getIP';
|
||||
import { Logger } from '../utils/logger';
|
||||
import { QueryCacher } from '../utils/queryCacher';
|
||||
import { getReputation } from '../utils/reputation';
|
||||
import { getCategoryActionType } from "../utils/categoryInfo";
|
||||
import { getHash } from "../utils/getHash";
|
||||
import { getIP } from "../utils/getIP";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import { getReputation } from "../utils/reputation";
|
||||
|
||||
|
||||
async function prepareCategorySegments(req: Request, videoID: VideoID, category: Category, segments: DBSegment[],cache: SegmentCache = {shadowHiddenSegmentIPs: {}}): Promise<Segment[]> {
|
||||
@@ -25,7 +25,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
|
||||
}
|
||||
|
||||
if (cache.shadowHiddenSegmentIPs[videoID] === undefined) {
|
||||
cache.shadowHiddenSegmentIPs[videoID] = await privateDB.prepare('all', 'SELECT "hashedIP" FROM "sponsorTimes" WHERE "videoID" = ?', [videoID]) as { hashedIP: HashedIP }[];
|
||||
cache.shadowHiddenSegmentIPs[videoID] = await privateDB.prepare("all", 'SELECT "hashedIP" FROM "sponsorTimes" WHERE "videoID" = ?', [videoID]) as { hashedIP: HashedIP }[];
|
||||
}
|
||||
|
||||
//if this isn't their ip, don't send it to them
|
||||
@@ -38,7 +38,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
|
||||
return shadowHiddenSegment.hashedIP === cache.userHashedIP;
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
const filteredSegments = segments.filter((_, index) => shouldFilter[index]);
|
||||
|
||||
const maxSegments = getCategoryActionType(category) === CategoryActionType.Skippable ? 32 : 1;
|
||||
@@ -51,8 +51,8 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, category:
|
||||
}));
|
||||
}
|
||||
|
||||
async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[],
|
||||
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
|
||||
async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories: Category[],
|
||||
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<Segment[]> {
|
||||
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
|
||||
const segments: Segment[] = [];
|
||||
|
||||
@@ -84,8 +84,8 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
|
||||
}
|
||||
}
|
||||
|
||||
async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[],
|
||||
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
|
||||
async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash, categories: Category[],
|
||||
actionTypes: ActionType[], requiredSegments: SegmentUUID[], service: Service): Promise<SBRecord<VideoID, VideoData>> {
|
||||
const cache: SegmentCache = {shadowHiddenSegmentIPs: {}};
|
||||
const segments: SBRecord<VideoID, VideoData> = {};
|
||||
|
||||
@@ -133,10 +133,10 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
|
||||
async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<DBSegment[]> {
|
||||
const fetchFromDB = () => db
|
||||
.prepare(
|
||||
'all',
|
||||
"all",
|
||||
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden", "hashedVideoID" FROM "sponsorTimes"
|
||||
WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
|
||||
[hashedVideoIDPrefix + '%', service]
|
||||
[`${hashedVideoIDPrefix}%`, service]
|
||||
) as Promise<DBSegment[]>;
|
||||
|
||||
if (hashedVideoIDPrefix.length === 4) {
|
||||
@@ -149,7 +149,7 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service
|
||||
async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
|
||||
const fetchFromDB = () => db
|
||||
.prepare(
|
||||
'all',
|
||||
"all",
|
||||
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "reputation", "shadowHidden" FROM "sponsorTimes"
|
||||
WHERE "videoID" = ? AND "service" = ? AND "hidden" = 0 ORDER BY "startTime"`,
|
||||
[videoID, service]
|
||||
@@ -283,7 +283,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
|
||||
? Array.isArray(req.query.category)
|
||||
? req.query.category
|
||||
: [req.query.category]
|
||||
: ['sponsor'];
|
||||
: ["sponsor"];
|
||||
if (!Array.isArray(categories)) {
|
||||
res.status(400).send("Categories parameter does not match format requirements.");
|
||||
return false;
|
||||
@@ -312,7 +312,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<Segment[]
|
||||
res.status(400).send("requiredSegments parameter does not match format requirements.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let service: Service = req.query.service ?? req.body.service ?? Service.YouTube;
|
||||
if (!Object.values(Service).some((val) => val == service)) {
|
||||
service = Service.YouTube;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {hashPrefixTester} from '../utils/hashPrefixTester';
|
||||
import {getSegmentsByHash} from './getSkipSegments';
|
||||
import {Request, Response} from 'express';
|
||||
import { ActionType, Category, SegmentUUID, Service, VideoIDHash } from '../types/segments.model';
|
||||
import {hashPrefixTester} from "../utils/hashPrefixTester";
|
||||
import {getSegmentsByHash} from "./getSkipSegments";
|
||||
import {Request, Response} from "express";
|
||||
import { ActionType, Category, SegmentUUID, Service, VideoIDHash } from "../types/segments.model";
|
||||
|
||||
export async function getSkipSegmentsByHash(req: Request, res: Response): Promise<Response> {
|
||||
let hashPrefix = req.params.prefix as VideoIDHash;
|
||||
@@ -18,7 +18,7 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
|
||||
? Array.isArray(req.query.category)
|
||||
? req.query.category
|
||||
: [req.query.category]
|
||||
: ['sponsor'];
|
||||
: ["sponsor"];
|
||||
if (!Array.isArray(categories)) {
|
||||
return res.status(400).send("Categories parameter does not match format requirements.");
|
||||
}
|
||||
@@ -45,12 +45,12 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
|
||||
let requiredSegments: SegmentUUID[] = [];
|
||||
try {
|
||||
requiredSegments = req.query.requiredSegments
|
||||
? JSON.parse(req.query.requiredSegments as string)
|
||||
: req.query.requiredSegment
|
||||
? Array.isArray(req.query.requiredSegment)
|
||||
? req.query.requiredSegment
|
||||
: [req.query.requiredSegment]
|
||||
: [];
|
||||
? JSON.parse(req.query.requiredSegments as string)
|
||||
: req.query.requiredSegment
|
||||
? Array.isArray(req.query.requiredSegment)
|
||||
? req.query.requiredSegment
|
||||
: [req.query.requiredSegment]
|
||||
: [];
|
||||
if (!Array.isArray(requiredSegments)) {
|
||||
return res.status(400).send("requiredSegments parameter does not match format requirements.");
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
|
||||
if (!Object.values(Service).some((val) => val == service)) {
|
||||
service = Service.YouTube;
|
||||
}
|
||||
|
||||
|
||||
// filter out none string elements, only flat array with strings is valid
|
||||
categories = categories.filter((item: any) => typeof item === "string");
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {createMemoryCache} from '../utils/createMemoryCache';
|
||||
import {config} from '../config';
|
||||
import {Request, Response} from 'express';
|
||||
import {db} from "../databases/databases";
|
||||
import {createMemoryCache} from "../utils/createMemoryCache";
|
||||
import {config} from "../config";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
const MILLISECONDS_IN_MINUTE = 60000;
|
||||
const getTopUsersWithCache = createMemoryCache(generateTopUsersStats, config.getTopUsersCacheTimeMinutes * MILLISECONDS_IN_MINUTE);
|
||||
@@ -14,7 +14,7 @@ async function generateTopUsersStats(sortBy: string, categoryStatsEnabled = fals
|
||||
const minutesSaved = [];
|
||||
const categoryStats: any[] = categoryStatsEnabled ? [] : undefined;
|
||||
|
||||
let additionalFields = '';
|
||||
let additionalFields = "";
|
||||
if (categoryStatsEnabled) {
|
||||
additionalFields += `SUM(CASE WHEN category = 'sponsor' THEN 1 ELSE 0 END) as "categorySponsor",
|
||||
SUM(CASE WHEN category = 'intro' THEN 1 ELSE 0 END) as "categorySumIntro",
|
||||
@@ -25,11 +25,9 @@ async function generateTopUsersStats(sortBy: string, categoryStatsEnabled = fals
|
||||
SUM(CASE WHEN category = 'preview' THEN 1 ELSE 0 END) as "categorySumPreview", `;
|
||||
}
|
||||
|
||||
const rows = await db.prepare('all', `SELECT COUNT(*) as "totalSubmissions", SUM(views) as "viewCount",
|
||||
const rows = await db.prepare("all", `SELECT COUNT(*) as "totalSubmissions", SUM(views) as "viewCount",
|
||||
SUM(((CASE WHEN "sponsorTimes"."endTime" - "sponsorTimes"."startTime" > ? THEN ? ELSE "sponsorTimes"."endTime" - "sponsorTimes"."startTime" END) / 60) * "sponsorTimes"."views") as "minutesSaved",
|
||||
SUM("votes") as "userVotes", ` +
|
||||
additionalFields +
|
||||
`COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
|
||||
SUM("votes") as "userVotes", ${additionalFields} COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
|
||||
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
|
||||
WHERE "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "shadowBannedUsers"."userID" IS NULL
|
||||
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 20
|
||||
@@ -73,13 +71,13 @@ export async function getTopUsers(req: Request, res: Response): Promise<Response
|
||||
}
|
||||
|
||||
//setup which sort type to use
|
||||
let sortBy = '';
|
||||
let sortBy = "";
|
||||
if (sortType == 0) {
|
||||
sortBy = 'minutesSaved';
|
||||
sortBy = "minutesSaved";
|
||||
} else if (sortType == 1) {
|
||||
sortBy = 'viewCount';
|
||||
sortBy = "viewCount";
|
||||
} else if (sortType == 2) {
|
||||
sortBy = 'totalSubmissions';
|
||||
sortBy = "totalSubmissions";
|
||||
} else {
|
||||
//invalid request
|
||||
return res.sendStatus(400);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {config} from '../config';
|
||||
import {Request, Response} from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {db} from "../databases/databases";
|
||||
import {config} from "../config";
|
||||
import {Request, Response} from "express";
|
||||
import fetch from "node-fetch";
|
||||
import {Logger} from "../utils/logger";
|
||||
|
||||
// A cache of the number of chrome web store users
|
||||
let chromeUsersCache = 0;
|
||||
@@ -16,7 +16,7 @@ let lastUserCountCheck = 0;
|
||||
export async function getTotalStats(req: Request, res: Response): Promise<void> {
|
||||
const userCountQuery = `(SELECT COUNT(*) FROM (SELECT DISTINCT "userID" from "sponsorTimes") t) "userCount",`;
|
||||
|
||||
const row = await db.prepare('get', `SELECT ${req.query.countContributingUsers ? userCountQuery : ""} COUNT(*) as "totalSubmissions",
|
||||
const row = await db.prepare("get", `SELECT ${req.query.countContributingUsers ? userCountQuery : ""} COUNT(*) as "totalSubmissions",
|
||||
SUM("views") as "viewCount", SUM(("endTime" - "startTime") / 60 * "views") as "minutesSaved" FROM "sponsorTimes" WHERE "shadowHidden" != 1 AND "votes" >= 0`, []);
|
||||
|
||||
if (row !== undefined) {
|
||||
@@ -44,42 +44,41 @@ export async function getTotalStats(req: Request, res: Response): Promise<void>
|
||||
|
||||
function updateExtensionUsers() {
|
||||
if (config.userCounterURL) {
|
||||
fetch(config.userCounterURL + "/api/v1/userCount")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
apiUsersCache = Math.max(apiUsersCache, data.userCount);
|
||||
})
|
||||
.catch(() => Logger.debug("Failing to connect to user counter at: " + config.userCounterURL));
|
||||
fetch(`${config.userCounterURL}/api/v1/userCount`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
apiUsersCache = Math.max(apiUsersCache, data.userCount);
|
||||
})
|
||||
.catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
|
||||
}
|
||||
|
||||
|
||||
const mozillaAddonsUrl = "https://addons.mozilla.org/api/v3/addons/addon/sponsorblock/";
|
||||
const chromeExtensionUrl = "https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone";
|
||||
|
||||
|
||||
fetch(mozillaAddonsUrl)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
firefoxUsersCache = data.average_daily_users;
|
||||
|
||||
fetch(chromeExtensionUrl)
|
||||
.then(res => res.text())
|
||||
.then(body => {
|
||||
// 2021-01-05
|
||||
// [...]<span><meta itemprop="interactionCount" content="UserDownloads:100.000+"/><meta itemprop="opera[...]
|
||||
const matchingString = '"UserDownloads:';
|
||||
const matchingStringLen = matchingString.length;
|
||||
const userDownloadsStartIndex = body.indexOf(matchingString);
|
||||
if (userDownloadsStartIndex >= 0) {
|
||||
const closingQuoteIndex = body.indexOf('"', userDownloadsStartIndex + matchingStringLen);
|
||||
const userDownloadsStr = body.substr(userDownloadsStartIndex + matchingStringLen, closingQuoteIndex - userDownloadsStartIndex).replace(',','').replace('.','');
|
||||
chromeUsersCache = parseInt(userDownloadsStr);
|
||||
}
|
||||
else {
|
||||
lastUserCountCheck = 0;
|
||||
}
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
firefoxUsersCache = data.average_daily_users;
|
||||
fetch(chromeExtensionUrl)
|
||||
.then(res => res.text())
|
||||
.then(body => {
|
||||
// 2021-01-05
|
||||
// [...]<span><meta itemprop="interactionCount" content="UserDownloads:100.000+"/><meta itemprop="opera[...]
|
||||
const matchingString = '"UserDownloads:';
|
||||
const matchingStringLen = matchingString.length;
|
||||
const userDownloadsStartIndex = body.indexOf(matchingString);
|
||||
if (userDownloadsStartIndex >= 0) {
|
||||
const closingQuoteIndex = body.indexOf('"', userDownloadsStartIndex + matchingStringLen);
|
||||
const userDownloadsStr = body.substr(userDownloadsStartIndex + matchingStringLen, closingQuoteIndex - userDownloadsStartIndex).replace(",","").replace(".","");
|
||||
chromeUsersCache = parseInt(userDownloadsStr);
|
||||
}
|
||||
else {
|
||||
lastUserCountCheck = 0;
|
||||
}
|
||||
})
|
||||
.catch(() => Logger.debug(`Failing to connect to ${chromeExtensionUrl}`));
|
||||
})
|
||||
.catch(() => Logger.debug("Failing to connect to " + chromeExtensionUrl));
|
||||
})
|
||||
.catch(() => {
|
||||
Logger.debug("Failing to connect to " + mozillaAddonsUrl);
|
||||
});
|
||||
.catch(() => {
|
||||
Logger.debug(`Failing to connect to ${mozillaAddonsUrl}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from 'express';
|
||||
import {UserID} from '../types/user.model';
|
||||
import {db} from "../databases/databases";
|
||||
import {Request, Response} from "express";
|
||||
import {UserID} from "../types/user.model";
|
||||
|
||||
function getFuzzyUserID(userName: string): Promise<{userName: string, userID: UserID }[]> {
|
||||
// escape [_ % \] to avoid ReDOS
|
||||
userName = userName.replace(/\\/g, '\\\\')
|
||||
.replace(/_/g, '\\_')
|
||||
.replace(/%/g, '\\%');
|
||||
userName = userName.replace(/\\/g, "\\\\")
|
||||
.replace(/_/g, "\\_")
|
||||
.replace(/%/g, "\\%");
|
||||
userName = `%${userName}%`; // add wildcard to username
|
||||
// LIMIT to reduce overhead | ESCAPE to escape LIKE wildcards
|
||||
try {
|
||||
return db.prepare('all', `SELECT "userName", "userID" FROM "userNames" WHERE "userName"
|
||||
return db.prepare("all", `SELECT "userName", "userID" FROM "userNames" WHERE "userName"
|
||||
LIKE ? ESCAPE '\\' LIMIT 10`, [userName]);
|
||||
} catch (err) {
|
||||
return null;
|
||||
@@ -19,7 +19,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
|
||||
|
||||
function getExactUserID(userName: string): Promise<{userName: string, userID: UserID }[]> {
|
||||
try {
|
||||
return db.prepare('all', `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]);
|
||||
return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export async function getUserID(req: Request, res: Response): Promise<Response>
|
||||
const exactSearch = req.query.exact
|
||||
? req.query.exact == "true"
|
||||
: false as boolean;
|
||||
|
||||
|
||||
// if not exact and length is 1, also skip
|
||||
if (userName == undefined || userName.length > 64 ||
|
||||
(!exactSearch && userName.length < 3)) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {Request, Response} from 'express';
|
||||
import {Logger} from '../utils/logger';
|
||||
import { HashedUserID, UserID } from '../types/user.model';
|
||||
import { getReputation } from '../utils/reputation';
|
||||
import {db} from "../databases/databases";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {Request, Response} from "express";
|
||||
import {Logger} from "../utils/logger";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { getReputation } from "../utils/reputation";
|
||||
import { SegmentUUID } from "../types/segments.model";
|
||||
|
||||
async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ minutesSaved: number, segmentCount: number }> {
|
||||
@@ -39,7 +39,7 @@ async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise<number> {
|
||||
|
||||
async function dbGetUsername(userID: HashedUserID) {
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
if (row !== undefined) {
|
||||
return row.userName;
|
||||
} else {
|
||||
@@ -53,7 +53,7 @@ async function dbGetUsername(userID: HashedUserID) {
|
||||
|
||||
async function dbGetViewsForUser(userID: HashedUserID) {
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID]);
|
||||
return row?.viewCount ?? 0;
|
||||
} catch (err) {
|
||||
return false;
|
||||
@@ -62,7 +62,7 @@ async function dbGetViewsForUser(userID: HashedUserID) {
|
||||
|
||||
async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID]);
|
||||
return row?.ignoredViewCount ?? 0;
|
||||
} catch (err) {
|
||||
return false;
|
||||
@@ -71,17 +71,17 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
|
||||
|
||||
async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID]);
|
||||
return row?.total ?? 0;
|
||||
} catch (err) {
|
||||
Logger.error('Couldn\'t get warnings for user ' + userID + '. returning 0');
|
||||
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUID> {
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID]);
|
||||
return row?.UUID ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
@@ -94,7 +94,7 @@ export async function getUserInfo(req: Request, res: Response): Promise<Response
|
||||
|
||||
if (hashedUserID == undefined) {
|
||||
//invalid request
|
||||
return res.status(400).send('Parameters are not valid');
|
||||
return res.status(400).send("Parameters are not valid");
|
||||
}
|
||||
|
||||
const segmentsSummary = await dbGetSubmittedSegmentSummary(hashedUserID);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {Request, Response} from 'express';
|
||||
import {db} from "../databases/databases";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
export async function getUsername(req: Request, res: Response): Promise<Response> {
|
||||
let userID = req.query.userID as string;
|
||||
@@ -15,7 +15,7 @@ export async function getUsername(req: Request, res: Response): Promise<Response
|
||||
userID = getHash(userID);
|
||||
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
|
||||
if (row !== undefined) {
|
||||
return res.send({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from 'express';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {db} from "../databases/databases";
|
||||
import {Request, Response} from "express";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {Logger} from "../utils/logger";
|
||||
|
||||
export async function getViewsForUser(req: Request, res: Response): Promise<Response> {
|
||||
let userID = req.query.userID as string;
|
||||
@@ -15,7 +15,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise<Resp
|
||||
userID = getHash(userID);
|
||||
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
|
||||
|
||||
//increase the view count by one
|
||||
if (row.viewCount != null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {handleGetSegments} from './getSkipSegments';
|
||||
import {Request, Response} from 'express';
|
||||
import {handleGetSegments} from "./getSkipSegments";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
export async function oldGetVideoSponsorTimes(req: Request, res: Response): Promise<Response> {
|
||||
const segments = await handleGetSegments(req, res);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {postSkipSegments} from './postSkipSegments';
|
||||
import {Request, Response} from 'express';
|
||||
import {postSkipSegments} from "./postSkipSegments";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
export async function oldSubmitSponsorTimes(req: Request, res: Response): Promise<Response> {
|
||||
req.query.category = "sponsor";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Logger } from '../utils/logger';
|
||||
import { HashedUserID, UserID } from '../types/user.model';
|
||||
import { getHash } from '../utils/getHash';
|
||||
import { Request, Response } from 'express';
|
||||
import { Service, VideoID } from '../types/segments.model';
|
||||
import { QueryCacher } from '../utils/queryCacher';
|
||||
import { isUserVIP } from '../utils/isUserVIP';
|
||||
import { Logger } from "../utils/logger";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { getHash } from "../utils/getHash";
|
||||
import { Request, Response } from "express";
|
||||
import { Service, VideoID } from "../types/segments.model";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import { isUserVIP } from "../utils/isUserVIP";
|
||||
import { VideoIDHash } from "../types/segments.model";
|
||||
|
||||
export async function postClearCache(req: Request, res: Response): Promise<Response> {
|
||||
@@ -13,17 +13,17 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
|
||||
const service = req.query.service as Service ?? Service.YouTube;
|
||||
|
||||
const invalidFields = [];
|
||||
if (typeof videoID !== 'string') {
|
||||
invalidFields.push('videoID');
|
||||
if (typeof videoID !== "string") {
|
||||
invalidFields.push("videoID");
|
||||
}
|
||||
if (typeof userID !== 'string') {
|
||||
invalidFields.push('userID');
|
||||
if (typeof userID !== "string") {
|
||||
invalidFields.push("userID");
|
||||
}
|
||||
|
||||
if (invalidFields.length !== 0) {
|
||||
// invalid request
|
||||
const fields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ', ' : '') + c, '');
|
||||
return res.status(400).send(`No valid ${fields} field(s) provided`);
|
||||
// invalid request
|
||||
const fields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ", " : "") + c, "");
|
||||
return res.status(400).send(`No valid ${fields} field(s) provided`);
|
||||
}
|
||||
|
||||
// hash the userID as early as possible
|
||||
@@ -33,7 +33,7 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
|
||||
|
||||
// Ensure user is a VIP
|
||||
if (!(await isUserVIP(hashedUserID))){
|
||||
Logger.warn("Permission violation: User " + hashedUserID + " attempted to clear cache for video " + videoID + ".");
|
||||
Logger.warn(`Permission violation: User ${hashedUserID} attempted to clear cache for video ${videoID}.`);
|
||||
return res.status(403).json({"message": "Not a VIP"});
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
|
||||
service
|
||||
});
|
||||
return res.status(200).json({
|
||||
message: "Cache cleared on video " + videoID
|
||||
message: `Cache cleared on video ${videoID}`
|
||||
});
|
||||
} catch(err) {
|
||||
return res.sendStatus(500);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Logger} from '../utils/logger';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from 'express';
|
||||
import {Logger} from "../utils/logger";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {db} from "../databases/databases";
|
||||
import {Request, Response} from "express";
|
||||
import { VideoIDHash } from "../types/segments.model";
|
||||
|
||||
export async function postLockCategories(req: Request, res: Response): Promise<string[]> {
|
||||
@@ -10,7 +10,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
const videoID = req.body.videoID;
|
||||
let userID = req.body.userID;
|
||||
const categories = req.body.categories;
|
||||
const reason: string = req.body.reason ?? '';
|
||||
const reason: string = req.body.reason ?? "";
|
||||
|
||||
// Check input data is valid
|
||||
if (!videoID
|
||||
@@ -20,7 +20,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
|| categories.length === 0
|
||||
) {
|
||||
res.status(400).json({
|
||||
message: 'Bad Format',
|
||||
message: "Bad Format",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -31,13 +31,13 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
|
||||
if (!userIsVIP) {
|
||||
res.status(403).json({
|
||||
message: 'Must be a VIP to mark videos.',
|
||||
message: "Must be a VIP to mark videos.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing lock categories markers
|
||||
let noCategoryList = await db.prepare('all', 'SELECT "category" from "lockCategories" where "videoID" = ?', [videoID]);
|
||||
let noCategoryList = await db.prepare("all", 'SELECT "category" from "lockCategories" where "videoID" = ?', [videoID]);
|
||||
if (!noCategoryList || noCategoryList.length === 0) {
|
||||
noCategoryList = [];
|
||||
} else {
|
||||
@@ -65,9 +65,9 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
// create database entry
|
||||
for (const category of categoriesToMark) {
|
||||
try {
|
||||
await db.prepare('run', `INSERT INTO "lockCategories" ("videoID", "userID", "category", "hashedVideoID", "reason") VALUES(?, ?, ?, ?, ?)`, [videoID, userID, category, hashedVideoID, reason]);
|
||||
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "category", "hashedVideoID", "reason") VALUES(?, ?, ?, ?, ?)`, [videoID, userID, category, hashedVideoID, reason]);
|
||||
} catch (err) {
|
||||
Logger.error("Error submitting 'lockCategories' marker for category '" + category + "' for video '" + videoID + "'");
|
||||
Logger.error(`Error submitting 'lockCategories' marker for category '${category}' for video '${videoID}'`);
|
||||
Logger.error(err);
|
||||
res.status(500).json({
|
||||
message: "Internal Server Error: Could not write marker to the database.",
|
||||
@@ -84,11 +84,11 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
|
||||
|
||||
for (const category of overlapCategories) {
|
||||
try {
|
||||
await db.prepare('run',
|
||||
await db.prepare("run",
|
||||
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "category" = ?',
|
||||
[reason, userID, videoID, category]);
|
||||
} catch (err) {
|
||||
Logger.error("Error submitting 'lockCategories' marker for category '" + category + "' for video '" + videoID + "'");
|
||||
Logger.error(`Error submitting 'lockCategories' marker for category '${category}' for video '${videoID}'`);
|
||||
Logger.error(err);
|
||||
res.status(500).json({
|
||||
message: "Internal Server Error: Could not write marker to the database.",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {Logger} from '../utils/logger';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {Request, Response} from 'express';
|
||||
import {HashedUserID, UserID} from '../types/user.model';
|
||||
import {Logger} from "../utils/logger";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {Request, Response} from "express";
|
||||
import {HashedUserID, UserID} from "../types/user.model";
|
||||
import {VideoID} from "../types/segments.model";
|
||||
import {db} from '../databases/databases';
|
||||
import {db} from "../databases/databases";
|
||||
|
||||
export async function postPurgeAllSegments(req: Request, res: Response): Promise<Response> {
|
||||
const userID = req.body.userID as UserID;
|
||||
@@ -22,12 +22,12 @@ export async function postPurgeAllSegments(req: Request, res: Response): Promise
|
||||
const vipState = await isUserVIP(hashedUserID);
|
||||
if (!vipState) {
|
||||
return res.status(403).json({
|
||||
message: 'Must be a VIP to perform this action.',
|
||||
message: "Must be a VIP to perform this action.",
|
||||
});
|
||||
}
|
||||
|
||||
await db.prepare('run', `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "videoID" = ?`, [videoID]);
|
||||
|
||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "videoID" = ?`, [videoID]);
|
||||
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
return res.sendStatus(500);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Request, Response} from 'express';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from "express";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {db} from "../databases/databases";
|
||||
|
||||
const ACTION_NONE = Symbol('none');
|
||||
const ACTION_UPDATE = Symbol('update');
|
||||
const ACTION_REMOVE = Symbol('remove');
|
||||
const ACTION_NONE = Symbol("none");
|
||||
const ACTION_UPDATE = Symbol("update");
|
||||
const ACTION_REMOVE = Symbol("remove");
|
||||
|
||||
function shiftSegment(segment: any, shift: { startTime: any; endTime: any }) {
|
||||
if (segment.startTime >= segment.endTime) return {action: ACTION_NONE, segment};
|
||||
@@ -59,7 +59,7 @@ export async function postSegmentShift(req: Request, res: Response): Promise<Res
|
||||
|| !endTime
|
||||
) {
|
||||
return res.status(400).json({
|
||||
message: 'Bad Format',
|
||||
message: "Bad Format",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@ export async function postSegmentShift(req: Request, res: Response): Promise<Res
|
||||
|
||||
if (!userIsVIP) {
|
||||
return res.status(403).json({
|
||||
message: 'Must be a VIP to perform this action.',
|
||||
message: "Must be a VIP to perform this action.",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const segments = await db.prepare('all', 'SELECT "startTime", "endTime", "UUID" FROM "sponsorTimes" WHERE "videoID" = ?', [videoID]);
|
||||
const segments = await db.prepare("all", 'SELECT "startTime", "endTime", "UUID" FROM "sponsorTimes" WHERE "videoID" = ?', [videoID]);
|
||||
const shift = {
|
||||
startTime,
|
||||
endTime,
|
||||
@@ -83,12 +83,12 @@ export async function postSegmentShift(req: Request, res: Response): Promise<Res
|
||||
for (const segment of segments) {
|
||||
const result = shiftSegment(segment, shift);
|
||||
switch (result.action) {
|
||||
case ACTION_UPDATE:
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET "startTime" = ?, "endTime" = ? WHERE "UUID" = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
|
||||
break;
|
||||
case ACTION_REMOVE:
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET "startTime" = ?, "endTime" = ?, "votes" = -2 WHERE "UUID" = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
|
||||
break;
|
||||
case ACTION_UPDATE:
|
||||
await db.prepare("run", 'UPDATE "sponsorTimes" SET "startTime" = ?, "endTime" = ? WHERE "UUID" = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
|
||||
break;
|
||||
case ACTION_REMOVE:
|
||||
await db.prepare("run", 'UPDATE "sponsorTimes" SET "startTime" = ?, "endTime" = ?, "votes" = -2 WHERE "UUID" = ?', [result.segment.startTime, result.segment.endTime, result.segment.UUID]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import {config} from '../config';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {db, privateDB} from '../databases/databases';
|
||||
import {getMaxResThumbnail, YouTubeAPI} from '../utils/youtubeApi';
|
||||
import {getSubmissionUUID} from '../utils/getSubmissionUUID';
|
||||
import fetch from 'node-fetch';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {getIP} from '../utils/getIP';
|
||||
import {getFormattedTime} from '../utils/getFormattedTime';
|
||||
import {isUserTrustworthy} from '../utils/isUserTrustworthy';
|
||||
import {dispatchEvent} from '../utils/webhookUtils';
|
||||
import {Request, Response} from 'express';
|
||||
import { ActionType, Category, CategoryActionType, IncomingSegment, SegmentUUID, Service, VideoDuration, VideoID } from '../types/segments.model';
|
||||
import { deleteLockCategories } from './deleteLockCategories';
|
||||
import { getCategoryActionType } from '../utils/categoryInfo';
|
||||
import { QueryCacher } from '../utils/queryCacher';
|
||||
import { getReputation } from '../utils/reputation';
|
||||
import { APIVideoData, APIVideoInfo } from '../types/youtubeApi.model';
|
||||
import { UserID } from '../types/user.model';
|
||||
import {config} from "../config";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {db, privateDB} from "../databases/databases";
|
||||
import {getMaxResThumbnail, YouTubeAPI} from "../utils/youtubeApi";
|
||||
import {getSubmissionUUID} from "../utils/getSubmissionUUID";
|
||||
import fetch from "node-fetch";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {getIP} from "../utils/getIP";
|
||||
import {getFormattedTime} from "../utils/getFormattedTime";
|
||||
import {isUserTrustworthy} from "../utils/isUserTrustworthy";
|
||||
import {dispatchEvent} from "../utils/webhookUtils";
|
||||
import {Request, Response} from "express";
|
||||
import { ActionType, Category, CategoryActionType, IncomingSegment, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model";
|
||||
import { deleteLockCategories } from "./deleteLockCategories";
|
||||
import { getCategoryActionType } from "../utils/categoryInfo";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import { getReputation } from "../utils/reputation";
|
||||
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
|
||||
async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: APIVideoData, {submissionStart, submissionEnd}: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) {
|
||||
const row = await db.prepare('get', `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
const userName = row !== undefined ? row.userName : null;
|
||||
|
||||
let scopeName = "submissions.other";
|
||||
@@ -32,7 +32,7 @@ async function sendWebhookNotification(userID: string, videoID: string, UUID: st
|
||||
"id": videoID,
|
||||
"title": youtubeData?.title,
|
||||
"thumbnail": getMaxResThumbnail(youtubeData) || null,
|
||||
"url": "https://www.youtube.com/watch?v=" + videoID,
|
||||
"url": `https://www.youtube.com/watch?v=${videoID}`,
|
||||
},
|
||||
"submission": {
|
||||
"UUID": UUID,
|
||||
@@ -49,7 +49,7 @@ async function sendWebhookNotification(userID: string, videoID: string, UUID: st
|
||||
|
||||
async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID: string, UUID: string, segmentInfo: any, service: Service) {
|
||||
if (apiVideoInfo && service == Service.YouTube) {
|
||||
const userSubmissionCountRow = await db.prepare('get', `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
|
||||
const userSubmissionCountRow = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
|
||||
|
||||
const {data, err} = apiVideoInfo;
|
||||
if (err) return;
|
||||
@@ -64,13 +64,13 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
|
||||
// If it is a first time submission
|
||||
// Then send a notification to discord
|
||||
if (config.discordFirstTimeSubmissionsWebhookURL === null || userSubmissionCountRow.submissionCount > 1) return;
|
||||
|
||||
|
||||
fetch(config.discordFirstTimeSubmissionsWebhookURL, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"embeds": [{
|
||||
"title": data?.title,
|
||||
"url": "https://www.youtube.com/watch?v=" + videoID + "&t=" + (parseInt(startTime.toFixed(0)) - 2),
|
||||
"url": `https://www.youtube.com/watch?v=${videoID}&t=${(parseInt(startTime.toFixed(0)) - 2)}`,
|
||||
"description": "Submission ID: " + UUID +
|
||||
"\n\nTimestamp: " +
|
||||
getFormattedTime(startTime) + " to " + getFormattedTime(endTime) +
|
||||
@@ -85,44 +85,44 @@ async function sendWebhooks(apiVideoInfo: APIVideoInfo, userID: string, videoID:
|
||||
}],
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending first time submission Discord hook");
|
||||
Logger.error(JSON.stringify(res));
|
||||
.then(res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending first time submission Discord hook");
|
||||
Logger.error(JSON.stringify(res));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send first time submission Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send first time submission Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function sendWebhooksNB(userID: string, videoID: string, UUID: string, startTime: number, endTime: number, category: string, probability: number, ytData: any) {
|
||||
const submissionInfoRow = await db.prepare('get', `SELECT
|
||||
const submissionInfoRow = await db.prepare("get", `SELECT
|
||||
(select count(1) from "sponsorTimes" where "userID" = ?) count,
|
||||
(select count(1) from "sponsorTimes" where "userID" = ? and "votes" <= -2) disregarded,
|
||||
coalesce((select "userName" FROM "userNames" WHERE "userID" = ?), ?) "userName"`,
|
||||
[userID, userID, userID, userID]);
|
||||
[userID, userID, userID, userID]);
|
||||
|
||||
let submittedBy: string;
|
||||
// If a userName was created then show both
|
||||
if (submissionInfoRow.userName !== userID) {
|
||||
submittedBy = submissionInfoRow.userName + "\n " + userID;
|
||||
submittedBy = `${submissionInfoRow.userName}\n${userID}`;
|
||||
} else {
|
||||
submittedBy = userID;
|
||||
}
|
||||
|
||||
// Send discord message
|
||||
if (config.discordNeuralBlockRejectWebhookURL === null) return;
|
||||
|
||||
|
||||
fetch(config.discordNeuralBlockRejectWebhookURL, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"embeds": [{
|
||||
"title": ytData.items[0].snippet.title,
|
||||
@@ -141,21 +141,21 @@ async function sendWebhooksNB(userID: string, videoID: string, UUID: string, sta
|
||||
}],
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending NeuralBlock Discord hook");
|
||||
Logger.error(JSON.stringify(res));
|
||||
.then(res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending NeuralBlock Discord hook");
|
||||
Logger.error(JSON.stringify(res));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send NeuralBlock Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send NeuralBlock Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// callback: function(reject: "String containing reason the submission was rejected")
|
||||
@@ -164,8 +164,8 @@ async function sendWebhooksNB(userID: string, videoID: string, UUID: string, sta
|
||||
// Looks like this was broken for no defined youtube key - fixed but IMO we shouldn't return
|
||||
// false for a pass - it was confusing and lead to this bug - any use of this function in
|
||||
// the future could have the same problem.
|
||||
async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
|
||||
submission: { videoID: VideoID; userID: UserID; segments: IncomingSegment[] }) {
|
||||
async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
|
||||
submission: { videoID: VideoID; userID: UserID; segments: IncomingSegment[] }) {
|
||||
if (apiVideoInfo) {
|
||||
const {err, data} = apiVideoInfo;
|
||||
if (err) return false;
|
||||
@@ -180,13 +180,13 @@ async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
|
||||
} else {
|
||||
if (segments[i].category === "sponsor") {
|
||||
//Prepare timestamps to send to NB all at once
|
||||
nbString = nbString + segments[i].segment[0] + "," + segments[i].segment[1] + ";";
|
||||
nbString = `${nbString}${segments[i].segment[0]},${segments[i].segment[1]};`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all submissions for this user
|
||||
const allSubmittedByUser = await db.prepare('all', `SELECT "startTime", "endTime" FROM "sponsorTimes" WHERE "userID" = ? and "videoID" = ? and "votes" > -1`, [submission.userID, submission.videoID]);
|
||||
const allSubmittedByUser = await db.prepare("all", `SELECT "startTime", "endTime" FROM "sponsorTimes" WHERE "userID" = ? and "videoID" = ? and "votes" > -1`, [submission.userID, submission.videoID]);
|
||||
const allSegmentTimes = [];
|
||||
if (allSubmittedByUser !== undefined) {
|
||||
//add segments the user has previously submitted
|
||||
@@ -221,8 +221,8 @@ async function autoModerateSubmission(apiVideoInfo: APIVideoInfo,
|
||||
// Check NeuralBlock
|
||||
const neuralBlockURL = config.neuralBlockURL;
|
||||
if (!neuralBlockURL) return false;
|
||||
const response = await fetch(neuralBlockURL + "/api/checkSponsorSegments?vid=" + submission.videoID +
|
||||
"&segments=" + nbString.substring(0, nbString.length - 1));
|
||||
const response = await fetch(`${neuralBlockURL}/api/checkSponsorSegments?vid=${submission.videoID}
|
||||
&segments=${nbString.substring(0, nbString.length - 1)}`);
|
||||
if (!response.ok) return false;
|
||||
|
||||
const nbPredictions = await response.json();
|
||||
@@ -270,7 +270,7 @@ async function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promi
|
||||
async function checkUserActiveWarning(userID: string): Promise<{ pass: boolean; errorMessage: string; }> {
|
||||
const MILLISECONDS_IN_HOUR = 3600000;
|
||||
const now = Date.now();
|
||||
const warnings = await db.prepare('all',
|
||||
const warnings = await db.prepare("all",
|
||||
`SELECT "reason"
|
||||
FROM warnings
|
||||
WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1
|
||||
@@ -278,30 +278,30 @@ async function checkUserActiveWarning(userID: string): Promise<{ pass: boolean;
|
||||
LIMIT ?`,
|
||||
[
|
||||
userID,
|
||||
Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR)),
|
||||
Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR)),
|
||||
config.maxNumberOfActiveWarnings
|
||||
],
|
||||
) as {reason: string}[];
|
||||
|
||||
if (warnings?.length >= config.maxNumberOfActiveWarnings) {
|
||||
const defaultMessage = 'Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. Could you please send a message in Discord or Matrix so we can further help you?';
|
||||
const defaultMessage = "Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. Could you please send a message in Discord or Matrix so we can further help you?";
|
||||
|
||||
return {
|
||||
pass: false,
|
||||
pass: false,
|
||||
errorMessage: warnings[0]?.reason?.length > 0 ? warnings[0].reason : defaultMessage
|
||||
};
|
||||
}
|
||||
|
||||
return {pass: true, errorMessage: ''};
|
||||
return {pass: true, errorMessage: ""};
|
||||
}
|
||||
|
||||
function proxySubmission(req: Request) {
|
||||
fetch(config.proxySubmission + '/api/skipSegments?userID=' + req.query.userID + '&videoID=' + req.query.videoID, {
|
||||
method: 'POST',
|
||||
body: req.body,
|
||||
})
|
||||
fetch(`${config.proxySubmission}/api/skipSegments?userID=${req.query.userID}&videoID=${req.query.videoID}`, {
|
||||
method: "POST",
|
||||
body: req.body,
|
||||
})
|
||||
.then(async res => {
|
||||
Logger.debug('Proxy Submission: ' + res.status + ' (' + (await res.text()) + ')');
|
||||
Logger.debug(`Proxy Submission: ${res.status} (${(await res.text())})`);
|
||||
})
|
||||
.catch(() => {
|
||||
Logger.error("Proxy Submission: Failed to make call");
|
||||
@@ -334,24 +334,24 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
segments.forEach((segment) => {
|
||||
if (!Object.values(ActionType).some((val) => val === segment.actionType)){
|
||||
segment.actionType = ActionType.Skip;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const invalidFields = [];
|
||||
if (typeof videoID !== 'string') {
|
||||
invalidFields.push('videoID');
|
||||
if (typeof videoID !== "string") {
|
||||
invalidFields.push("videoID");
|
||||
}
|
||||
if (typeof userID !== 'string' || userID.length < 30) {
|
||||
invalidFields.push('userID');
|
||||
if (typeof userID !== "string" || userID.length < 30) {
|
||||
invalidFields.push("userID");
|
||||
}
|
||||
if (!Array.isArray(segments) || segments.length < 1) {
|
||||
invalidFields.push('segments');
|
||||
invalidFields.push("segments");
|
||||
}
|
||||
|
||||
if (invalidFields.length !== 0) {
|
||||
// invalid request
|
||||
const fields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ', ' : '') + c, '');
|
||||
return res.status(400).send(`No valid ${fields} field(s) provided`);
|
||||
// invalid request
|
||||
const fields = invalidFields.reduce((p, c, i) => p + (i !== 0 ? ", " : "") + c, "");
|
||||
return res.status(400).send(`No valid ${fields} field(s) provided`);
|
||||
}
|
||||
|
||||
//hash the userID
|
||||
@@ -362,14 +362,14 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
return res.status(403).send(warningResult.errorMessage);
|
||||
}
|
||||
|
||||
let lockedCategoryList = (await db.prepare('all', 'SELECT category from "lockCategories" where "videoID" = ?', [videoID])).map((list: any) => list.category );
|
||||
let lockedCategoryList = (await db.prepare("all", 'SELECT category from "lockCategories" where "videoID" = ?', [videoID])).map((list: any) => list.category );
|
||||
|
||||
//check if this user is on the vip list
|
||||
const isVIP = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [userID])).userCount > 0;
|
||||
|
||||
const decreaseVotes = 0;
|
||||
|
||||
const previousSubmissions = await db.prepare('all',
|
||||
const previousSubmissions = await db.prepare("all",
|
||||
`SELECT "videoDuration", "UUID"
|
||||
FROM "sponsorTimes"
|
||||
WHERE "videoID" = ? AND "service" = ? AND
|
||||
@@ -395,7 +395,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
if (videoDurationChanged(videoDuration)) {
|
||||
// Hide all previous submissions
|
||||
for (const submission of previousSubmissions) {
|
||||
await db.prepare('run', `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]);
|
||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1 WHERE "UUID" = ?`, [submission.UUID]);
|
||||
}
|
||||
|
||||
// Reset lock categories
|
||||
@@ -417,7 +417,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
// Reject segment if it's in the locked categories list
|
||||
if (!isVIP && lockedCategoryList.indexOf(segments[i].category) !== -1) {
|
||||
// TODO: Do something about the fradulent submission
|
||||
Logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'");
|
||||
Logger.warn(`Caught a no-segment submission. userID: '${userID}', videoID: '${videoID}', category: '${segments[i].category}'`);
|
||||
return res.status(403).send(
|
||||
"New submissions are not allowed for the following category: '"
|
||||
+ segments[i].category + "'. A moderator has decided that no new segments are needed and that all current segments of this category are timed perfectly.\n\n "
|
||||
@@ -433,7 +433,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
|
||||
if (isNaN(startTime) || isNaN(endTime)
|
||||
|| startTime === Infinity || endTime === Infinity || startTime < 0 || startTime > endTime
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.Skippable && startTime === endTime)
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.Skippable && startTime === endTime)
|
||||
|| (getCategoryActionType(segments[i].category) === CategoryActionType.POI && startTime !== endTime)) {
|
||||
//invalid request
|
||||
return res.status(400).send("One of your segments times are invalid (too short, startTime before endTime, etc.)");
|
||||
@@ -445,7 +445,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
}
|
||||
|
||||
//check if this info has already been submitted before
|
||||
const duplicateCheck2Row = await db.prepare('get', `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "startTime" = ?
|
||||
const duplicateCheck2Row = await db.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "startTime" = ?
|
||||
and "endTime" = ? and "category" = ? and "videoID" = ? and "service" = ?`, [startTime, endTime, segments[i].category, videoID, service]);
|
||||
if (duplicateCheck2Row.count > 0) {
|
||||
return res.sendStatus(409);
|
||||
@@ -463,7 +463,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
//decreaseVotes = -2; //Disable for now
|
||||
} else if (autoModerateResult) {
|
||||
//Normal automod behavior
|
||||
return res.status(403).send("Request rejected by auto moderator: " + autoModerateResult + " If this is an issue, send a message on Discord.");
|
||||
return res.status(403).send(`Request rejected by auto moderator: ${autoModerateResult} If this is an issue, send a message on Discord.`);
|
||||
}
|
||||
}
|
||||
// Will be filled when submitting
|
||||
@@ -483,7 +483,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false) {
|
||||
//check to see if this ip has submitted too many sponsors today
|
||||
const rateLimitCheckRow = await privateDB.prepare('get', `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "hashedIP" = ? AND "videoID" = ? AND "timeSubmitted" > ?`, [hashedIP, videoID, yesterday]);
|
||||
const rateLimitCheckRow = await privateDB.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "hashedIP" = ? AND "videoID" = ? AND "timeSubmitted" > ?`, [hashedIP, videoID, yesterday]);
|
||||
|
||||
if (rateLimitCheckRow.count >= 10) {
|
||||
//too many sponsors for the same video from the same ip address
|
||||
@@ -495,7 +495,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false) {
|
||||
//check to see if the user has already submitted sponsors for this video
|
||||
const duplicateCheckRow = await db.prepare('get', `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "userID" = ? and "videoID" = ?`, [userID, videoID]);
|
||||
const duplicateCheckRow = await db.prepare("get", `SELECT COUNT(*) as count FROM "sponsorTimes" WHERE "userID" = ? and "videoID" = ?`, [userID, videoID]);
|
||||
|
||||
if (duplicateCheckRow.count >= 16) {
|
||||
//too many sponsors for the same video from the same user
|
||||
@@ -504,7 +504,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
}
|
||||
|
||||
//check to see if this user is shadowbanned
|
||||
const shadowBanRow = await db.prepare('get', `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||
const shadowBanRow = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||
|
||||
let shadowBanned = shadowBanRow.userCount;
|
||||
|
||||
@@ -525,16 +525,16 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
|
||||
const startingLocked = isVIP ? 1 : 0;
|
||||
try {
|
||||
await db.prepare('run', `INSERT INTO "sponsorTimes"
|
||||
await db.prepare("run", `INSERT INTO "sponsorTimes"
|
||||
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID")
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
||||
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, shadowBanned, hashedVideoID,
|
||||
],
|
||||
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, shadowBanned, hashedVideoID,
|
||||
],
|
||||
);
|
||||
|
||||
//add to private db as well
|
||||
await privateDB.prepare('run', `INSERT INTO "sponsorTimes" VALUES(?, ?, ?)`, [videoID, hashedIP, timeSubmitted]);
|
||||
|
||||
await privateDB.prepare("run", `INSERT INTO "sponsorTimes" VALUES(?, ?, ?)`, [videoID, hashedIP, timeSubmitted]);
|
||||
|
||||
// Clear redis cache for this video
|
||||
QueryCacher.clearVideoCache({
|
||||
videoID,
|
||||
@@ -544,8 +544,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
});
|
||||
} catch (err) {
|
||||
//a DB change probably occurred
|
||||
Logger.error("Error when putting sponsorTime in the DB: " + videoID + ", " + segmentInfo.segment[0] + ", " +
|
||||
segmentInfo.segment[1] + ", " + userID + ", " + segmentInfo.category + ". " + err);
|
||||
Logger.error(`Error when putting sponsorTime in the DB: ${videoID}, ${segmentInfo.segment[0]}, ${segmentInfo.segment[1]}, ${userID}, ${segmentInfo.category}. ${err}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
@@ -567,18 +566,18 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
return res.json(newSegments);
|
||||
}
|
||||
|
||||
// Takes an array of arrays:
|
||||
// ex)
|
||||
// Takes an array of arrays:
|
||||
// ex)
|
||||
// [
|
||||
// [3, 40],
|
||||
// [50, 70],
|
||||
// [60, 80],
|
||||
// [3, 40],
|
||||
// [50, 70],
|
||||
// [60, 80],
|
||||
// [100, 150]
|
||||
// ]
|
||||
// ]
|
||||
// => transforms to combining overlapping segments
|
||||
// [
|
||||
// [3, 40],
|
||||
// [50, 80],
|
||||
// [50, 80],
|
||||
// [100, 150]
|
||||
// ]
|
||||
function mergeTimeSegments(ranges: number[][]) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Request, Response} from 'express';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {db} from '../databases/databases';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import { HashedUserID, UserID } from '../types/user.model';
|
||||
import {Request, Response} from "express";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {db} from "../databases/databases";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
|
||||
export async function postWarning(req: Request, res: Response): Promise<Response> {
|
||||
// Collect user input data
|
||||
@@ -11,22 +11,22 @@ export async function postWarning(req: Request, res: Response): Promise<Response
|
||||
const userID: UserID = req.body.userID;
|
||||
const issueTime = new Date().getTime();
|
||||
const enabled: boolean = req.body.enabled ?? true;
|
||||
const reason: string = req.body.reason ?? '';
|
||||
const reason: string = req.body.reason ?? "";
|
||||
|
||||
// Ensure user is a VIP
|
||||
if (!await isUserVIP(issuerUserID)) {
|
||||
Logger.warn("Permission violation: User " + issuerUserID + " attempted to warn user " + userID + ".");
|
||||
Logger.warn(`Permission violation: User ${issuerUserID} attempted to warn user ${userID}.`);
|
||||
return res.status(403).json({"message": "Not a VIP"});
|
||||
}
|
||||
|
||||
let resultStatus = "";
|
||||
|
||||
if (enabled) {
|
||||
const previousWarning = await db.prepare('get', 'SELECT * FROM "warnings" WHERE "userID" = ? AND "issuerUserID" = ?', [userID, issuerUserID]);
|
||||
const previousWarning = await db.prepare("get", 'SELECT * FROM "warnings" WHERE "userID" = ? AND "issuerUserID" = ?', [userID, issuerUserID]);
|
||||
|
||||
if (!previousWarning) {
|
||||
await db.prepare(
|
||||
'run',
|
||||
"run",
|
||||
'INSERT INTO "warnings" ("userID", "issueTime", "issuerUserID", "enabled", "reason") VALUES (?, ?, ?, 1, ?)',
|
||||
[userID, issueTime, issuerUserID, reason]
|
||||
);
|
||||
@@ -35,11 +35,11 @@ export async function postWarning(req: Request, res: Response): Promise<Response
|
||||
return res.sendStatus(409);
|
||||
}
|
||||
} else {
|
||||
await db.prepare('run', 'UPDATE "warnings" SET "enabled" = 0 WHERE "userID" = ?', [userID]);
|
||||
await db.prepare("run", 'UPDATE "warnings" SET "enabled" = 0 WHERE "userID" = ?', [userID]);
|
||||
resultStatus = "removed from";
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Warning " + resultStatus + " user '" + userID + "'.",
|
||||
message: `Warning ${resultStatus} user '${userID}'.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {config} from '../config';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {db, privateDB} from '../databases/databases';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {Request, Response} from 'express';
|
||||
import {config} from "../config";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {db, privateDB} from "../databases/databases";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
async function logUserNameChange(userID: string, newUserName: string, oldUserName: string, updatedByAdmin: boolean): Promise<Response> {
|
||||
return privateDB.prepare('run',
|
||||
return privateDB.prepare("run",
|
||||
`INSERT INTO "userNameLogs"("userID", "newUserName", "oldUserName", "updatedByAdmin", "updatedAt") VALUES(?, ?, ?, ?, ?)`,
|
||||
[userID, newUserName, oldUserName, + updatedByAdmin, new Date().getTime()]
|
||||
);
|
||||
@@ -26,11 +26,11 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
||||
// Don't allow
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
|
||||
// remove unicode control characters from username (example: \n, \r, \t etc.)
|
||||
// source: https://en.wikipedia.org/wiki/Control_character#In_Unicode
|
||||
// eslint-disable-next-line no-control-regex
|
||||
userName = userName.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
|
||||
userName = userName.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
|
||||
|
||||
if (adminUserIDInput != undefined) {
|
||||
//this is the admin controlling the other users account, don't hash the controling account's ID
|
||||
@@ -46,7 +46,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await db.prepare('get', `SELECT count(*) as count FROM "userNames" WHERE "userID" = ? AND "locked" = '1'`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT count(*) as count FROM "userNames" WHERE "userID" = ? AND "locked" = '1'`, [userID]);
|
||||
if (adminUserIDInput === undefined && row.count > 0) {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
@@ -58,17 +58,17 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
||||
|
||||
try {
|
||||
//check if username is already set
|
||||
const row = await db.prepare('get', `SELECT "userName" FROM "userNames" WHERE "userID" = ? LIMIT 1`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ? LIMIT 1`, [userID]);
|
||||
const locked = adminUserIDInput === undefined ? 0 : 1;
|
||||
let oldUserName = '';
|
||||
let oldUserName = "";
|
||||
|
||||
if (row?.userName?.length > 0) {
|
||||
//already exists, update this row
|
||||
oldUserName = row.userName;
|
||||
await db.prepare('run', `UPDATE "userNames" SET "userName" = ?, "locked" = ? WHERE "userID" = ?`, [userName, locked, userID]);
|
||||
await db.prepare("run", `UPDATE "userNames" SET "userName" = ?, "locked" = ? WHERE "userID" = ?`, [userName, locked, userID]);
|
||||
} else {
|
||||
//add to the db
|
||||
await db.prepare('run', `INSERT INTO "userNames"("userID", "userName", "locked") VALUES(?, ?, ?)`, [userID, userName, locked]);
|
||||
await db.prepare("run", `INSERT INTO "userNames"("userID", "userName", "locked") VALUES(?, ?, ?)`, [userID, userName, locked]);
|
||||
}
|
||||
|
||||
await logUserNameChange(userID, userName, oldUserName, adminUserIDInput !== undefined);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {Request, Response} from 'express';
|
||||
import { config } from '../config';
|
||||
import { Category, Service, VideoID, VideoIDHash } from '../types/segments.model';
|
||||
import { UserID } from '../types/user.model';
|
||||
import { QueryCacher } from '../utils/queryCacher';
|
||||
import {db} from "../databases/databases";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {Request, Response} from "express";
|
||||
import { config } from "../config";
|
||||
import { Category, Service, VideoID, VideoIDHash } from "../types/segments.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
|
||||
export async function shadowBanUser(req: Request, res: Response): Promise<Response> {
|
||||
const userID = req.query.userID as string;
|
||||
@@ -13,7 +13,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise<Respon
|
||||
|
||||
const enabled = req.query.enabled === undefined
|
||||
? true
|
||||
: req.query.enabled === 'true';
|
||||
: req.query.enabled === "true";
|
||||
|
||||
//if enabled is false and the old submissions should be made visible again
|
||||
const unHideOldSubmissions = req.query.unHideOldSubmissions !== "false";
|
||||
@@ -37,50 +37,50 @@ export async function shadowBanUser(req: Request, res: Response): Promise<Respon
|
||||
|
||||
if (userID) {
|
||||
//check to see if this user is already shadowbanned
|
||||
const row = await db.prepare('get', `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||
|
||||
if (enabled && row.userCount == 0) {
|
||||
//add them to the shadow ban list
|
||||
|
||||
//add it to the table
|
||||
await db.prepare('run', `INSERT INTO "shadowBannedUsers" VALUES(?)`, [userID]);
|
||||
await db.prepare("run", `INSERT INTO "shadowBannedUsers" VALUES(?)`, [userID]);
|
||||
|
||||
//find all previous submissions and hide them
|
||||
if (unHideOldSubmissions) {
|
||||
await db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 1 WHERE "userID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")})
|
||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "shadowHidden" = 1 WHERE "userID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")})
|
||||
AND NOT EXISTS ( SELECT "videoID", "category" FROM "lockCategories" WHERE
|
||||
"sponsorTimes"."videoID" = "lockCategories"."videoID" AND "sponsorTimes"."category" = "lockCategories"."category")`, [userID]);
|
||||
|
||||
|
||||
// clear cache for all old videos
|
||||
(await db.prepare('all', `SELECT "videoID", "hashedVideoID", "service", "votes", "views" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]))
|
||||
(await db.prepare("all", `SELECT "videoID", "hashedVideoID", "service", "votes", "views" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]))
|
||||
.forEach((videoInfo: {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID}) => {
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
} else if (!enabled && row.userCount > 0) {
|
||||
//remove them from the shadow ban list
|
||||
await db.prepare('run', `DELETE FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||
await db.prepare("run", `DELETE FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID]);
|
||||
|
||||
//find all previous submissions and unhide them
|
||||
if (unHideOldSubmissions) {
|
||||
const segmentsToIgnore = (await db.prepare('all', `SELECT "UUID" FROM "sponsorTimes" st
|
||||
JOIN "lockCategories" ns on "st"."videoID" = "ns"."videoID" AND st.category = ns.category WHERE "st"."userID" = ?`
|
||||
, [userID])).map((item: {UUID: string}) => item.UUID);
|
||||
const allSegments = (await db.prepare('all', `SELECT "UUID" FROM "sponsorTimes" st WHERE "st"."userID" = ?`, [userID]))
|
||||
.map((item: {UUID: string}) => item.UUID);
|
||||
const segmentsToIgnore = (await db.prepare("all", `SELECT "UUID" FROM "sponsorTimes" st
|
||||
JOIN "lockCategories" ns on "st"."videoID" = "ns"."videoID" AND st.category = ns.category WHERE "st"."userID" = ?`
|
||||
, [userID])).map((item: {UUID: string}) => item.UUID);
|
||||
const allSegments = (await db.prepare("all", `SELECT "UUID" FROM "sponsorTimes" st WHERE "st"."userID" = ?`, [userID]))
|
||||
.map((item: {UUID: string}) => item.UUID);
|
||||
|
||||
await Promise.all(allSegments.filter((item: {uuid: string}) => {
|
||||
return segmentsToIgnore.indexOf(item) === -1;
|
||||
}).map(async (UUID: string) => {
|
||||
// collect list for unshadowbanning
|
||||
(await db.prepare('all', `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ? AND "shadowHidden" = 1 AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID]))
|
||||
(await db.prepare("all", `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ? AND "shadowHidden" = 1 AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID]))
|
||||
.forEach((videoInfo: {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID}) => {
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
return db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 0 WHERE "UUID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID]);
|
||||
return db.prepare("run", `UPDATE "sponsorTimes" SET "shadowHidden" = 0 WHERE "UUID" = ? AND "category" in (${categories.map((c) => `'${c}'`).join(",")})`, [UUID]);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export async function shadowBanUser(req: Request, res: Response): Promise<Respon
|
||||
|
||||
//find all previous submissions and hide them
|
||||
if (unHideOldSubmissions) {
|
||||
await db.prepare('run', `UPDATE "sponsorTimes" SET "shadowHidden" = 1 WHERE "timeSubmitted" IN
|
||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "shadowHidden" = 1 WHERE "timeSubmitted" IN
|
||||
(SELECT "privateDB"."timeSubmitted" FROM "sponsorTimes" LEFT JOIN "privateDB"."sponsorTimes" as "privateDB" ON "sponsorTimes"."timeSubmitted"="privateDB"."timeSubmitted"
|
||||
WHERE "privateDB"."hashedIP" = ?)`, [hashedIP]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {db} from '../databases/databases';
|
||||
import {Request, Response} from 'express';
|
||||
import {db} from "../databases/databases";
|
||||
import {Request, Response} from "express";
|
||||
|
||||
export async function viewedVideoSponsorTime(req: Request, res: Response): Promise<Response> {
|
||||
const UUID = req.query.UUID;
|
||||
@@ -10,7 +10,7 @@ export async function viewedVideoSponsorTime(req: Request, res: Response): Promi
|
||||
}
|
||||
|
||||
//up the view count by one
|
||||
await db.prepare('run', `UPDATE "sponsorTimes" SET views = views + 1 WHERE "UUID" = ?`, [UUID]);
|
||||
await db.prepare("run", `UPDATE "sponsorTimes" SET views = views + 1 WHERE "UUID" = ?`, [UUID]);
|
||||
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import {Request, Response} from 'express';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import fetch from 'node-fetch';
|
||||
import {getMaxResThumbnail, YouTubeAPI} from '../utils/youtubeApi';
|
||||
import {db, privateDB} from '../databases/databases';
|
||||
import {dispatchEvent, getVoteAuthor, getVoteAuthorRaw} from '../utils/webhookUtils';
|
||||
import {getFormattedTime} from '../utils/getFormattedTime';
|
||||
import {getIP} from '../utils/getIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {config} from '../config';
|
||||
import { UserID } from '../types/user.model';
|
||||
import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from '../types/segments.model';
|
||||
import { getCategoryActionType } from '../utils/categoryInfo';
|
||||
import { QueryCacher } from '../utils/queryCacher';
|
||||
import {Request, Response} from "express";
|
||||
import {Logger} from "../utils/logger";
|
||||
import {isUserVIP} from "../utils/isUserVIP";
|
||||
import fetch from "node-fetch";
|
||||
import {getMaxResThumbnail, YouTubeAPI} from "../utils/youtubeApi";
|
||||
import {db, privateDB} from "../databases/databases";
|
||||
import {dispatchEvent, getVoteAuthor, getVoteAuthorRaw} from "../utils/webhookUtils";
|
||||
import {getFormattedTime} from "../utils/getFormattedTime";
|
||||
import {getIP} from "../utils/getIP";
|
||||
import {getHash} from "../utils/getHash";
|
||||
import {config} from "../config";
|
||||
import { UserID } from "../types/user.model";
|
||||
import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from "../types/segments.model";
|
||||
import { getCategoryActionType } from "../utils/categoryInfo";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
|
||||
const voteTypes = {
|
||||
normal: 0,
|
||||
@@ -49,26 +49,25 @@ interface VoteData {
|
||||
}
|
||||
|
||||
async function sendWebhooks(voteData: VoteData) {
|
||||
const submissionInfoRow = await db.prepare('get', `SELECT "s"."videoID", "s"."userID", s."startTime", s."endTime", s."category", u."userName",
|
||||
const submissionInfoRow = await db.prepare("get", `SELECT "s"."videoID", "s"."userID", s."startTime", s."endTime", s."category", u."userName",
|
||||
(select count(1) from "sponsorTimes" where "userID" = s."userID") count,
|
||||
(select count(1) from "sponsorTimes" where "userID" = s."userID" and votes <= -2) disregarded
|
||||
FROM "sponsorTimes" s left join "userNames" u on s."userID" = u."userID" where s."UUID"=?`,
|
||||
[voteData.UUID]);
|
||||
[voteData.UUID]);
|
||||
|
||||
const userSubmissionCountRow = await db.prepare('get', `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [voteData.nonAnonUserID]);
|
||||
const userSubmissionCountRow = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [voteData.nonAnonUserID]);
|
||||
|
||||
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
|
||||
let webhookURL: string = null;
|
||||
if (voteData.voteTypeEnum === voteTypes.normal) {
|
||||
switch (voteData.finalResponse.webhookType) {
|
||||
case VoteWebhookType.Normal:
|
||||
webhookURL = config.discordReportChannelWebhookURL;
|
||||
break;
|
||||
case VoteWebhookType.Rejected:
|
||||
webhookURL = config.discordFailedReportChannelWebhookURL;
|
||||
break;
|
||||
case VoteWebhookType.Normal:
|
||||
webhookURL = config.discordReportChannelWebhookURL;
|
||||
break;
|
||||
case VoteWebhookType.Rejected:
|
||||
webhookURL = config.discordFailedReportChannelWebhookURL;
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (voteData.voteTypeEnum === voteTypes.incorrect) {
|
||||
webhookURL = config.discordCompletelyIncorrectReportWebhookURL;
|
||||
}
|
||||
@@ -86,7 +85,7 @@ async function sendWebhooks(voteData: VoteData) {
|
||||
"video": {
|
||||
"id": submissionInfoRow.videoID,
|
||||
"title": data?.title,
|
||||
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID,
|
||||
"url": `https://www.youtube.com/watch?v=${submissionInfoRow.videoID}`,
|
||||
"thumbnail": getMaxResThumbnail(data) || null,
|
||||
},
|
||||
"submission": {
|
||||
@@ -113,12 +112,11 @@ async function sendWebhooks(voteData: VoteData) {
|
||||
// Send discord message
|
||||
if (webhookURL !== null && !isUpvote) {
|
||||
fetch(webhookURL, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"embeds": [{
|
||||
"title": data?.title,
|
||||
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID
|
||||
+ "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2),
|
||||
"url": `https://www.youtube.com/watch?v=${submissionInfoRow.videoID}&t=${(submissionInfoRow.startTime.toFixed(0) - 2)}`,
|
||||
"description": "**" + voteData.row.votes + " Votes Prior | " +
|
||||
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
|
||||
+ " Views**\n\n**Submission ID:** " + voteData.UUID
|
||||
@@ -140,38 +138,38 @@ async function sendWebhooks(voteData: VoteData) {
|
||||
}],
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending reported submission Discord hook");
|
||||
Logger.error(JSON.stringify((await res.text())));
|
||||
.then(async res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending reported submission Discord hook");
|
||||
Logger.error(JSON.stringify((await res.text())));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send reported submission Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send reported submission Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, isOwnSubmission: boolean, category: Category
|
||||
, hashedIP: HashedIP, finalResponse: FinalResponse, res: Response): Promise<Response> {
|
||||
, hashedIP: HashedIP, finalResponse: FinalResponse, res: Response): Promise<Response> {
|
||||
// Check if they've already made a vote
|
||||
const usersLastVoteInfo = await privateDB.prepare('get', `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID]);
|
||||
const usersLastVoteInfo = await privateDB.prepare("get", `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID]);
|
||||
|
||||
if (usersLastVoteInfo?.category === category) {
|
||||
// Double vote, ignore
|
||||
return res.sendStatus(finalResponse.finalStatus);
|
||||
}
|
||||
|
||||
const videoInfo = (await db.prepare('get', `SELECT "category", "videoID", "hashedVideoID", "service", "userID" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
||||
[UUID])) as {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID};
|
||||
const videoInfo = (await db.prepare("get", `SELECT "category", "videoID", "hashedVideoID", "service", "userID" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
||||
[UUID])) as {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID};
|
||||
if (!videoInfo) {
|
||||
// Submission doesn't exist
|
||||
return res.status(400).send("Submission doesn't exist.");
|
||||
@@ -193,22 +191,22 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
|
||||
if (ableToVote) {
|
||||
// Add the vote
|
||||
if ((await db.prepare('get', `select count(*) as count from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category])).count > 0) {
|
||||
if ((await db.prepare("get", `select count(*) as count from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category])).count > 0) {
|
||||
// Update the already existing db entry
|
||||
await db.prepare('run', `update "categoryVotes" set "votes" = "votes" + ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, category]);
|
||||
await db.prepare("run", `update "categoryVotes" set "votes" = "votes" + ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, category]);
|
||||
} else {
|
||||
// Add a db entry
|
||||
await db.prepare('run', `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, category, voteAmount]);
|
||||
await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, category, voteAmount]);
|
||||
}
|
||||
|
||||
// Add the info into the private db
|
||||
if (usersLastVoteInfo?.votes > 0) {
|
||||
// Reverse the previous vote
|
||||
await db.prepare('run', `update "categoryVotes" set "votes" = "votes" - ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, usersLastVoteInfo.category]);
|
||||
await db.prepare("run", `update "categoryVotes" set "votes" = "votes" - ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, usersLastVoteInfo.category]);
|
||||
|
||||
await privateDB.prepare('run', `update "categoryVotes" set "category" = ?, "timeSubmitted" = ?, "hashedIP" = ? where "userID" = ? and "UUID" = ?`, [category, timeSubmitted, hashedIP, userID, UUID]);
|
||||
await privateDB.prepare("run", `update "categoryVotes" set "category" = ?, "timeSubmitted" = ?, "hashedIP" = ? where "userID" = ? and "UUID" = ?`, [category, timeSubmitted, hashedIP, userID, UUID]);
|
||||
} else {
|
||||
await privateDB.prepare('run', `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, category, timeSubmitted]);
|
||||
await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, category, timeSubmitted]);
|
||||
}
|
||||
|
||||
// See if the submissions category is ready to change
|
||||
@@ -235,7 +233,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
// VIPs change it every time
|
||||
if (nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2) || isVIP || isOwnSubmission) {
|
||||
// Replace the category
|
||||
await db.prepare('run', `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
|
||||
await db.prepare("run", `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +277,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
const hashedIP: HashedIP = getHash((ip + config.globalSalt) as IPAddress);
|
||||
|
||||
//check if this user is on the vip list
|
||||
const isVIP = (await db.prepare('get', `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [nonAnonUserID])).userCount > 0;
|
||||
const isVIP = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [nonAnonUserID])).userCount > 0;
|
||||
|
||||
//check if user voting on own submission
|
||||
const isOwnSubmission = (await db.prepare("get", `SELECT "UUID" as "submissionCount" FROM "sponsorTimes" where "userID" = ? AND "UUID" = ?`, [nonAnonUserID, UUID])) !== undefined;
|
||||
@@ -289,13 +287,13 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
// no longer allow type 10/11 alternative votes
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
|
||||
// If not upvote
|
||||
if (!isVIP && type !== 1) {
|
||||
const isSegmentLocked = async () => !!(await db.prepare('get', `SELECT "locked" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]))?.locked;
|
||||
const isVideoLocked = async () => !!(await db.prepare('get', 'SELECT "lockCategories".category from "lockCategories" left join "sponsorTimes"' +
|
||||
' on ("lockCategories"."videoID" = "sponsorTimes"."videoID" and "lockCategories".category = "sponsorTimes".category)' +
|
||||
' where "UUID" = ?', [UUID]));
|
||||
const isSegmentLocked = async () => !!(await db.prepare("get", `SELECT "locked" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]))?.locked;
|
||||
const isVideoLocked = async () => !!(await db.prepare("get", `SELECT "lockCategories".category from "lockCategories" left join "sponsorTimes"
|
||||
on ("lockCategories"."videoID" = "sponsorTimes"."videoID" and "lockCategories".category = "sponsorTimes".category)
|
||||
where "UUID" = ?`, [UUID]));
|
||||
|
||||
if (await isSegmentLocked() || await isVideoLocked()) {
|
||||
finalResponse.blockVote = true;
|
||||
@@ -310,7 +308,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
|
||||
if (type !== undefined && !isVIP && !isOwnSubmission) {
|
||||
// Check if upvoting hidden segment
|
||||
const voteInfo = await db.prepare('get', `SELECT votes FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
||||
const voteInfo = await db.prepare("get", `SELECT votes FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
||||
|
||||
if (voteInfo && voteInfo.votes <= -2) {
|
||||
if (type == 1) {
|
||||
@@ -324,19 +322,19 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
|
||||
const MILLISECONDS_IN_HOUR = 3600000;
|
||||
const now = Date.now();
|
||||
const warningsCount = (await db.prepare('get', `SELECT count(*) as count FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
|
||||
const warningsCount = (await db.prepare("get", `SELECT count(*) as count FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
|
||||
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
|
||||
)).count;
|
||||
|
||||
if (warningsCount >= config.maxNumberOfActiveWarnings) {
|
||||
return res.status(403).send('Vote rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. Could you please send a message in Discord or Matrix so we can further help you?');
|
||||
return res.status(403).send("Vote rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. Could you please send a message in Discord or Matrix so we can further help you?");
|
||||
}
|
||||
|
||||
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
|
||||
|
||||
try {
|
||||
//check if vote has already happened
|
||||
const votesRow = await privateDB.prepare('get', `SELECT "type" FROM "votes" WHERE "userID" = ? AND "UUID" = ?`, [userID, UUID]);
|
||||
const votesRow = await privateDB.prepare("get", `SELECT "type" FROM "votes" WHERE "userID" = ? AND "UUID" = ?`, [userID, UUID]);
|
||||
|
||||
//-1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
|
||||
let incrementAmount = 0;
|
||||
@@ -381,7 +379,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
}
|
||||
|
||||
//check if the increment amount should be multiplied (downvotes have more power if there have been many views)
|
||||
const videoInfo = await db.prepare('get', `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
|
||||
const videoInfo = await db.prepare("get", `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
|
||||
{videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, votes: number, views: number, userID: UserID};
|
||||
|
||||
if (voteTypeEnum === voteTypes.normal) {
|
||||
@@ -410,9 +408,9 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
if (ableToVote) {
|
||||
//update the votes table
|
||||
if (votesRow != undefined) {
|
||||
await privateDB.prepare('run', `UPDATE "votes" SET "type" = ? WHERE "userID" = ? AND "UUID" = ?`, [type, userID, UUID]);
|
||||
await privateDB.prepare("run", `UPDATE "votes" SET "type" = ? WHERE "userID" = ? AND "UUID" = ?`, [type, userID, UUID]);
|
||||
} else {
|
||||
await privateDB.prepare('run', `INSERT INTO "votes" VALUES(?, ?, ?, ?)`, [UUID, userID, hashedIP, type]);
|
||||
await privateDB.prepare("run", `INSERT INTO "votes" VALUES(?, ?, ?, ?)`, [UUID, userID, hashedIP, type]);
|
||||
}
|
||||
|
||||
let columnName = "";
|
||||
@@ -424,13 +422,13 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
|
||||
//update the vote count on this sponsorTime
|
||||
//oldIncrementAmount will be zero is row is null
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET "' + columnName + '" = "' + columnName + '" + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]);
|
||||
await db.prepare("run", `UPDATE "sponsorTimes" SET "${columnName}" = "${columnName}" + ? WHERE "UUID" = ?`, [incrementAmount - oldIncrementAmount, UUID]);
|
||||
if (isVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
|
||||
// Unide and Lock this submission
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 1, hidden = 0 WHERE "UUID" = ?', [UUID]);
|
||||
await db.prepare("run", 'UPDATE "sponsorTimes" SET locked = 1, hidden = 0 WHERE "UUID" = ?', [UUID]);
|
||||
} else if (isVIP && incrementAmount < 0 && voteTypeEnum === voteTypes.normal) {
|
||||
// Unlock if a VIP downvotes it
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 0 WHERE "UUID" = ?', [UUID]);
|
||||
// Unlock if a VIP downvotes it
|
||||
await db.prepare("run", 'UPDATE "sponsorTimes" SET locked = 0 WHERE "UUID" = ?', [UUID]);
|
||||
}
|
||||
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
@@ -452,6 +450,6 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
|
||||
return res.status(finalResponse.finalStatus).send(finalResponse.finalMessage ?? undefined);
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
return res.status(500).json({error: 'Internal error creating segment vote'});
|
||||
return res.status(500).json({error: "Internal error creating segment vote"});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PoolConfig } from 'pg';
|
||||
import * as redis from 'redis';
|
||||
import { PoolConfig } from "pg";
|
||||
import * as redis from "redis";
|
||||
import { CacheOptions } from "@ajayyy/lru-diskcache";
|
||||
|
||||
export interface SBSConfig {
|
||||
|
||||
@@ -17,8 +17,8 @@ export enum ActionType {
|
||||
|
||||
// Uncomment as needed
|
||||
export enum Service {
|
||||
YouTube = 'YouTube',
|
||||
PeerTube = 'PeerTube',
|
||||
YouTube = "YouTube",
|
||||
PeerTube = "PeerTube",
|
||||
// Twitch = 'Twitch',
|
||||
// Nebula = 'Nebula',
|
||||
// RSS = 'RSS',
|
||||
@@ -26,16 +26,16 @@ export enum Service {
|
||||
// Lbry = 'Lbry'
|
||||
}
|
||||
|
||||
export interface IncomingSegment {
|
||||
category: Category;
|
||||
export interface IncomingSegment {
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
segment: string[];
|
||||
segment: string[];
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
category: Category;
|
||||
export interface Segment {
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
segment: number[];
|
||||
segment: number[];
|
||||
UUID: SegmentUUID;
|
||||
videoDuration: VideoDuration;
|
||||
}
|
||||
@@ -45,8 +45,8 @@ export enum Visibility {
|
||||
HIDDEN = 1
|
||||
}
|
||||
|
||||
export interface DBSegment {
|
||||
category: Category;
|
||||
export interface DBSegment {
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
|
||||
@@ -10,24 +10,24 @@ export interface APIVideoData {
|
||||
"height": number
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
"description": string,
|
||||
"descriptionHtml": string,
|
||||
"published": number,
|
||||
"publishedText": string,
|
||||
|
||||
|
||||
"keywords": string[],
|
||||
"viewCount": number,
|
||||
"likeCount": number,
|
||||
"dislikeCount": number,
|
||||
|
||||
|
||||
"paid": boolean,
|
||||
"premium": boolean,
|
||||
"isFamilyFriendly": boolean,
|
||||
"allowedRegions": string[],
|
||||
"genre": string,
|
||||
"genreUrl": string,
|
||||
|
||||
|
||||
"author": string,
|
||||
"authorId": string,
|
||||
"authorUrl": string,
|
||||
@@ -38,7 +38,7 @@ export interface APIVideoData {
|
||||
"height": number
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
"subCountText": string,
|
||||
"lengthSeconds": number,
|
||||
"allowRatings": boolean,
|
||||
@@ -47,7 +47,7 @@ export interface APIVideoData {
|
||||
"liveNow": boolean,
|
||||
"isUpcoming": boolean,
|
||||
"premiereTimestamp"?: number,
|
||||
|
||||
|
||||
"hlsUrl"?: string,
|
||||
"adaptiveFormats": [
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Category, CategoryActionType } from "../types/segments.model";
|
||||
|
||||
export function getCategoryActionType(category: Category): CategoryActionType {
|
||||
switch (category) {
|
||||
case "highlight":
|
||||
return CategoryActionType.POI;
|
||||
default:
|
||||
return CategoryActionType.Skippable;
|
||||
case "highlight":
|
||||
return CategoryActionType.POI;
|
||||
default:
|
||||
return CategoryActionType.Skippable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function createMemoryCache(memoryFn: (...args: any[]) => void, cacheTimeM
|
||||
const promiseMemory = new Map();
|
||||
return (...args: any[]) => {
|
||||
// create cacheKey by joining arguments as string
|
||||
const cacheKey = args.join('.');
|
||||
const cacheKey = args.join(".");
|
||||
// check if promising is already running
|
||||
if (promiseMemory.has(cacheKey)) {
|
||||
return promiseMemory.get(cacheKey);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { config } from "../config";
|
||||
let DiskCache: LRU<string, string>;
|
||||
|
||||
if (config.diskCache) {
|
||||
DiskCache = new LRU('./databases/cache', config.diskCache);
|
||||
DiskCache = new LRU("./databases/cache", config.diskCache);
|
||||
DiskCache.init();
|
||||
} else {
|
||||
DiskCache = {
|
||||
|
||||
@@ -7,8 +7,8 @@ export function getFormattedTime(totalSeconds: number): string {
|
||||
let secondsDisplay = seconds.toFixed(3);
|
||||
if (seconds < 10) {
|
||||
//add a zero
|
||||
secondsDisplay = '0' + secondsDisplay;
|
||||
secondsDisplay = `0${secondsDisplay}`;
|
||||
}
|
||||
|
||||
return minutes + ':' + secondsDisplay;
|
||||
return `${minutes}:${secondsDisplay}`;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import crypto from 'crypto';
|
||||
import { HashedValue } from '../types/hash.model';
|
||||
import crypto from "crypto";
|
||||
import { HashedValue } from "../types/hash.model";
|
||||
|
||||
export function getHash<T extends string>(value: T, times = 5000): T & HashedValue {
|
||||
if (times <= 0) return "" as T & HashedValue;
|
||||
|
||||
for (let i = 0; i < times; i++) {
|
||||
const hashCreator = crypto.createHash('sha256');
|
||||
value = hashCreator.update(value).digest('hex') as T;
|
||||
const hashCreator = crypto.createHash("sha256");
|
||||
value = hashCreator.update(value).digest("hex") as T;
|
||||
}
|
||||
|
||||
return value as T & HashedValue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {config} from '../config';
|
||||
import {Request} from 'express';
|
||||
import { IPAddress } from '../types/segments.model';
|
||||
import {config} from "../config";
|
||||
import {Request} from "express";
|
||||
import { IPAddress } from "../types/segments.model";
|
||||
|
||||
export function getIP(req: Request): IPAddress {
|
||||
if (config.behindProxy === true || config.behindProxy === "true") {
|
||||
@@ -8,14 +8,13 @@ export function getIP(req: Request): IPAddress {
|
||||
}
|
||||
|
||||
switch (config.behindProxy as string) {
|
||||
case "X-Forwarded-For":
|
||||
return req.headers['x-forwarded-for'] as IPAddress;
|
||||
case "Cloudflare":
|
||||
return req.headers['cf-connecting-ip'] as IPAddress;
|
||||
case "X-Real-IP":
|
||||
return req.headers['x-real-ip'] as IPAddress;
|
||||
default:
|
||||
return req.connection.remoteAddress as IPAddress;
|
||||
case "X-Forwarded-For":
|
||||
return req.headers["x-forwarded-for"] as IPAddress;
|
||||
case "Cloudflare":
|
||||
return req.headers["cf-connecting-ip"] as IPAddress;
|
||||
case "X-Real-IP":
|
||||
return req.headers["x-real-ip"] as IPAddress;
|
||||
default:
|
||||
return req.connection.remoteAddress as IPAddress;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {getHash} from './getHash';
|
||||
import { HashedValue } from '../types/hash.model';
|
||||
import { ActionType, Category, VideoID } from '../types/segments.model';
|
||||
import { UserID } from '../types/user.model';
|
||||
import {getHash} from "./getHash";
|
||||
import { HashedValue } from "../types/hash.model";
|
||||
import { ActionType, VideoID } from "../types/segments.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
|
||||
export function getSubmissionUUID(videoID: VideoID, actionType: ActionType, userID: UserID, startTime: number, endTime: number): HashedValue{
|
||||
return `3${getHash('v3' + videoID + startTime + endTime + userID, 1)}` as HashedValue;
|
||||
return `3${getHash(`v3${videoID}${startTime}${endTime}${userID}`, 1)}` as HashedValue;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {config} from '../config';
|
||||
import {config} from "../config";
|
||||
|
||||
const minimumPrefix = config.minimumPrefix || '3';
|
||||
const maximumPrefix = config.maximumPrefix || '32'; // Half the hash.
|
||||
const minimumPrefix = config.minimumPrefix || "3";
|
||||
const maximumPrefix = config.maximumPrefix || "32"; // Half the hash.
|
||||
|
||||
const prefixChecker = new RegExp('^[\\da-f]{' + minimumPrefix + ',' + maximumPrefix + '}$', 'i');
|
||||
const prefixChecker = new RegExp(`^[\\da-f]{${minimumPrefix},${maximumPrefix}}$`, "i");
|
||||
|
||||
export function hashPrefixTester(prefix: string): boolean {
|
||||
return prefixChecker.test(prefix);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {db} from '../databases/databases';
|
||||
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
|
||||
@@ -6,11 +6,11 @@ import {db} from '../databases/databases';
|
||||
*/
|
||||
export async function isUserTrustworthy(userID: string): Promise<boolean> {
|
||||
//check to see if this user how many submissions this user has submitted
|
||||
const totalSubmissionsRow = await db.prepare('get', `SELECT count(*) as "totalSubmissions", sum(votes) as "voteSum" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
|
||||
const totalSubmissionsRow = await 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 = await db.prepare('get', `SELECT count(*) as "downvotedSubmissions" FROM "sponsorTimes" WHERE "userID" = ? AND (votes < 0 OR "shadowHidden" > 0)`, [userID]);
|
||||
const downvotedSubmissionsRow = await 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);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {db} from '../databases/databases';
|
||||
import { HashedUserID } from '../types/user.model';
|
||||
import {db} from "../databases/databases";
|
||||
import { HashedUserID } from "../types/user.model";
|
||||
|
||||
export async function isUserVIP(userID: HashedUserID): Promise<boolean> {
|
||||
return (await db.prepare('get', `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [userID])).userCount > 0;
|
||||
return (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ?`, [userID])).userCount > 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {config} from '../config';
|
||||
import {config} from "../config";
|
||||
|
||||
const enum LogLevel {
|
||||
ERROR = "ERROR",
|
||||
@@ -45,10 +45,10 @@ class Logger {
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (config.mode === 'development') {
|
||||
if (config.mode === "development") {
|
||||
this._settings.INFO = true;
|
||||
this._settings.DEBUG = true;
|
||||
} else if (config.mode === 'test') {
|
||||
} else if (config.mode === "test") {
|
||||
this._settings.WARN = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ async function get<T>(fetchFromDB: () => Promise<T>, key: string): Promise<T> {
|
||||
|
||||
if (!err && reply) {
|
||||
try {
|
||||
Logger.debug("Got data from redis: " + reply);
|
||||
Logger.debug(`Got data from redis: ${reply}`);
|
||||
return JSON.parse(reply);
|
||||
} catch (e) {
|
||||
// If all else, continue on to fetching from the database
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {config} from '../config';
|
||||
import {Logger} from './logger';
|
||||
import redis, {Callback} from 'redis';
|
||||
import {config} from "../config";
|
||||
import {Logger} from "./logger";
|
||||
import redis, {Callback} from "redis";
|
||||
|
||||
interface RedisSB {
|
||||
get(key: string, callback?: Callback<string | null>): void;
|
||||
@@ -12,17 +12,17 @@ interface RedisSB {
|
||||
|
||||
let exportObject: RedisSB = {
|
||||
get: (key, callback?) => callback(null, undefined),
|
||||
getAsync: () =>
|
||||
getAsync: () =>
|
||||
new Promise((resolve) => resolve({err: null, reply: undefined})),
|
||||
set: (key, value, callback) => callback(null, undefined),
|
||||
setAsync: () =>
|
||||
setAsync: () =>
|
||||
new Promise((resolve) => resolve({err: null, reply: undefined})),
|
||||
delAsync: () =>
|
||||
delAsync: () =>
|
||||
new Promise((resolve) => resolve(null)),
|
||||
};
|
||||
|
||||
if (config.redis) {
|
||||
Logger.info('Connected to redis');
|
||||
Logger.info("Connected to redis");
|
||||
const client = redis.createClient(config.redis);
|
||||
exportObject = client;
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ import { UserID } from "../types/user.model";
|
||||
import { Logger } from "./logger";
|
||||
|
||||
export function skipSegmentsKey(videoID: VideoID, service: Service): string {
|
||||
return "segments.v2." + service + ".videoID." + videoID;
|
||||
}
|
||||
return `segments.v2.${service}.videoID.${videoID}`;
|
||||
}
|
||||
|
||||
export function skipSegmentsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string {
|
||||
hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash;
|
||||
if (hashedVideoIDPrefix.length !== 4) Logger.warn("Redis skip segment hash-prefix key is not length 4! " + hashedVideoIDPrefix);
|
||||
|
||||
return "segments.v2." + service + "." + hashedVideoIDPrefix;
|
||||
}
|
||||
if (hashedVideoIDPrefix.length !== 4) Logger.warn(`Redis skip segment hash-prefix key is not length 4! ${hashedVideoIDPrefix}`);
|
||||
|
||||
return `segments.v2.${ service }.${ hashedVideoIDPrefix}`;
|
||||
}
|
||||
|
||||
export function reputationKey(userID: UserID): string {
|
||||
return "reputation.user." + userID;
|
||||
}
|
||||
return `reputation.user.${ userID}`;
|
||||
}
|
||||
|
||||
@@ -15,21 +15,21 @@ interface ReputationDBResult {
|
||||
export async function getReputation(userID: UserID): Promise<number> {
|
||||
const pastDate = Date.now() - 1000 * 60 * 60 * 24 * 45; // 45 days ago
|
||||
// 1596240000000 is August 1st 2020, a little after auto upvote was disabled
|
||||
const fetchFromDB = () => db.prepare("get",
|
||||
`SELECT COUNT(*) AS "totalSubmissions",
|
||||
SUM(CASE WHEN "votes" < 0 THEN 1 ELSE 0 END) AS "downvotedSubmissions",
|
||||
SUM(CASE WHEN "votes" < 0 AND "videoID" NOT IN
|
||||
(SELECT b."videoID" FROM "sponsorTimes" as b
|
||||
WHERE b."userID" = ?
|
||||
AND b."votes" > 0 AND b."category" = "a"."category" AND b."videoID" = "a"."videoID" LIMIT 1)
|
||||
THEN 1 ELSE 0 END) AS "nonSelfDownvotedSubmissions",
|
||||
SUM(CASE WHEN "votes" > 0 AND "timeSubmitted" > 1596240000000 THEN "votes" ELSE 0 END) AS "upvotedSum",
|
||||
SUM(locked) AS "lockedSum",
|
||||
SUM(CASE WHEN "timeSubmitted" < ? AND "timeSubmitted" > 1596240000000 AND "votes" > 0 THEN 1 ELSE 0 END) AS "oldUpvotedSubmissions"
|
||||
FROM "sponsorTimes" as "a" WHERE "userID" = ?`, [userID, pastDate, userID]) as Promise<ReputationDBResult>;
|
||||
const fetchFromDB = () => db.prepare("get",
|
||||
`SELECT COUNT(*) AS "totalSubmissions",
|
||||
SUM(CASE WHEN "votes" < 0 THEN 1 ELSE 0 END) AS "downvotedSubmissions",
|
||||
SUM(CASE WHEN "votes" < 0 AND "videoID" NOT IN
|
||||
(SELECT b."videoID" FROM "sponsorTimes" as b
|
||||
WHERE b."userID" = ?
|
||||
AND b."votes" > 0 AND b."category" = "a"."category" AND b."videoID" = "a"."videoID" LIMIT 1)
|
||||
THEN 1 ELSE 0 END) AS "nonSelfDownvotedSubmissions",
|
||||
SUM(CASE WHEN "votes" > 0 AND "timeSubmitted" > 1596240000000 THEN "votes" ELSE 0 END) AS "upvotedSum",
|
||||
SUM(locked) AS "lockedSum",
|
||||
SUM(CASE WHEN "timeSubmitted" < ? AND "timeSubmitted" > 1596240000000 AND "votes" > 0 THEN 1 ELSE 0 END) AS "oldUpvotedSubmissions"
|
||||
FROM "sponsorTimes" as "a" WHERE "userID" = ?`, [userID, pastDate, userID]) as Promise<ReputationDBResult>;
|
||||
|
||||
const result = await QueryCacher.get(fetchFromDB, reputationKey(userID));
|
||||
|
||||
|
||||
// Grace period
|
||||
if (result.totalSubmissions < 5) {
|
||||
return 0;
|
||||
@@ -53,7 +53,7 @@ export async function getReputation(userID: UserID): Promise<number> {
|
||||
}
|
||||
|
||||
function convertRange(value: number, currentMin: number, currentMax: number, targetMin: number, targetMax: number): number {
|
||||
const currentRange = currentMax - currentMin;
|
||||
const currentRange = currentMax - currentMin;
|
||||
const targetRange = targetMax - targetMin;
|
||||
return ((value - currentMin) / currentRange) * targetRange + targetMin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {config} from '../config';
|
||||
import {Logger} from '../utils/logger';
|
||||
import fetch from 'node-fetch';
|
||||
import {config} from "../config";
|
||||
import {Logger} from "../utils/logger";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
function getVoteAuthorRaw(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string {
|
||||
if (isOwnSubmission) {
|
||||
@@ -38,16 +38,15 @@ function dispatchEvent(scope: string, data: Record<string, unknown>): void {
|
||||
if (!scopes.includes(scope.toLowerCase())) return;
|
||||
|
||||
fetch(webhookURL, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Authorization": authKey,
|
||||
"Event-Type": scope, // Maybe change this in the future?
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.warn('Couldn\'t send webhook to ' + webhook.url);
|
||||
}).catch(err => {
|
||||
Logger.warn(`Couldn't send webhook to ${webhook.url}`);
|
||||
Logger.warn(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fetch from 'node-fetch';
|
||||
import {config} from '../config';
|
||||
import {Logger} from './logger';
|
||||
import { APIVideoData, APIVideoInfo } from '../types/youtubeApi.model';
|
||||
import DiskCache from './diskCache';
|
||||
import fetch from "node-fetch";
|
||||
import {config} from "../config";
|
||||
import {Logger} from "./logger";
|
||||
import { APIVideoData, APIVideoInfo } from "../types/youtubeApi.model";
|
||||
import DiskCache from "./diskCache";
|
||||
|
||||
export class YouTubeAPI {
|
||||
static async listVideos(videoID: string, ignoreCache = false): Promise<APIVideoInfo> {
|
||||
@@ -10,13 +10,13 @@ export class YouTubeAPI {
|
||||
return { err: "Invalid video ID" };
|
||||
}
|
||||
|
||||
const cacheKey = "yt.newleaf.video." + videoID;
|
||||
const cacheKey = `yt.newleaf.video.$[videoID}`;
|
||||
if (!ignoreCache) {
|
||||
try {
|
||||
const data = await DiskCache.get(cacheKey);
|
||||
|
||||
if (data) {
|
||||
Logger.debug("YouTube API: cache used for video information: " + videoID);
|
||||
Logger.debug(`YouTube API: cache used for video information: ${videoID}`);
|
||||
return { err: null, data: JSON.parse(data) };
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -27,26 +27,26 @@ export class YouTubeAPI {
|
||||
if (!config.newLeafURLs || config.newLeafURLs.length <= 0) return {err: "NewLeaf URL not found", data: null};
|
||||
|
||||
try {
|
||||
const result = await fetch(config.newLeafURLs[Math.floor(Math.random() * config.newLeafURLs.length)] + "/api/v1/videos/" + videoID, { method: "GET" });
|
||||
const result = await fetch(`${config.newLeafURLs[Math.floor(Math.random() * config.newLeafURLs.length)]}/api/v1/videos/${videoID}`, { method: "GET" });
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
if (data.error) {
|
||||
Logger.warn("NewLeaf API Error for " + videoID + ": " + data.error);
|
||||
Logger.warn(`NewLeaf API Error for ${videoID}: ${data.error}`);
|
||||
return { err: data.error, data: null };
|
||||
}
|
||||
|
||||
DiskCache.set(cacheKey, JSON.stringify(data))
|
||||
.catch((err: any) => Logger.warn(err))
|
||||
.then(() => Logger.debug("YouTube API: video information cache set for: " + videoID));
|
||||
|
||||
.catch((err: any) => Logger.warn(err))
|
||||
.then(() => Logger.debug(`YouTube API: video information cache set for: ${videoID}`));
|
||||
|
||||
return { err: false, data };
|
||||
} else {
|
||||
return { err: result.statusText, data: null };
|
||||
}
|
||||
} catch (err) {
|
||||
return {err, data: null};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user