mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-06 11:36:58 +03:00
Merge branch 'master' into fix/requiredSegments-hidden
# Conflicts: # src/routes/getSkipSegments.ts
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
|
export interface QueryOption {
|
||||||
|
useReplica?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDatabase {
|
export interface IDatabase {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
|
|
||||||
prepare(type: QueryType, query: string, params?: any[]): Promise<any | any[] | void>;
|
prepare(type: QueryType, query: string, params?: any[], options?: QueryOption): Promise<any | any[] | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryType = "get" | "all" | "run";
|
export type QueryType = "get" | "all" | "run";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Logger } from "../utils/logger";
|
import { Logger } from "../utils/logger";
|
||||||
import { IDatabase, QueryType } from "./IDatabase";
|
import { IDatabase, QueryOption, QueryType } from "./IDatabase";
|
||||||
import { Client, Pool, PoolClient, types } from "pg";
|
import { Client, Pool, PoolClient, types } from "pg";
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -32,6 +32,8 @@ export class Postgres implements IDatabase {
|
|||||||
private poolRead: Pool;
|
private poolRead: Pool;
|
||||||
private lastPoolReadFail = 0;
|
private lastPoolReadFail = 0;
|
||||||
|
|
||||||
|
private maxTries = 3;
|
||||||
|
|
||||||
constructor(private config: DatabaseConfig) {}
|
constructor(private config: DatabaseConfig) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
@@ -82,7 +84,7 @@ export class Postgres implements IDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepare(type: QueryType, query: string, params?: any[]): Promise<any[]> {
|
async prepare(type: QueryType, query: string, params?: any[], options: QueryOption = {}): Promise<any[]> {
|
||||||
// Convert query to use numbered parameters
|
// Convert query to use numbered parameters
|
||||||
let count = 1;
|
let count = 1;
|
||||||
for (let char = 0; char < query.length; char++) {
|
for (let char = 0; char < query.length; char++) {
|
||||||
@@ -94,46 +96,47 @@ export class Postgres implements IDatabase {
|
|||||||
|
|
||||||
Logger.debug(`prepare (postgres): type: ${type}, query: ${query}, params: ${params}`);
|
Logger.debug(`prepare (postgres): type: ${type}, query: ${query}, params: ${params}`);
|
||||||
|
|
||||||
let client: PoolClient;
|
let tries = 0;
|
||||||
try {
|
do {
|
||||||
client = await this.getClient(type);
|
tries++;
|
||||||
const queryResult = await client.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): ${JSON.stringify(values)}`);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
case "run": {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Logger.error(`prepare (postgres): ${err}`);
|
|
||||||
} finally {
|
|
||||||
try {
|
try {
|
||||||
client?.release();
|
const queryResult = await this.getPool(type, options).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): ${JSON.stringify(values)}`);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
case "run": {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`prepare (postgres): ${err}`);
|
if (err instanceof Error && err.message.includes("terminating connection due to conflict with recovery")) {
|
||||||
|
options.useReplica = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.error(`prepare (postgres) try ${tries}: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
} while ((type === "get" || type === "all") && tries < this.maxTries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getClient(type: string): Promise<PoolClient> {
|
private getPool(type: string, options: QueryOption): Pool {
|
||||||
const readAvailable = this.poolRead && (type === "get" || type === "all");
|
const readAvailable = this.poolRead && options.useReplica && (type === "get" || type === "all");
|
||||||
const ignroreReadDueToFailure = this.lastPoolReadFail > Date.now() - 1000 * 30;
|
const ignroreReadDueToFailure = this.lastPoolReadFail > Date.now() - 1000 * 30;
|
||||||
const readDueToFailure = this.lastPoolFail > Date.now() - 1000 * 30;
|
const readDueToFailure = this.lastPoolFail > Date.now() - 1000 * 30;
|
||||||
if (readAvailable && !ignroreReadDueToFailure && (readDueToFailure ||
|
if (readAvailable && !ignroreReadDueToFailure && (readDueToFailure ||
|
||||||
Math.random() > 1 / (this.config.postgresReadOnly.weight + 1))) {
|
Math.random() > 1 / (this.config.postgresReadOnly.weight + 1))) {
|
||||||
return this.poolRead.connect();
|
return this.poolRead;
|
||||||
} else {
|
} else {
|
||||||
return this.pool.connect();
|
return this.pool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export async function getSavedTimeForUser(req: Request, res: Response): Promise<
|
|||||||
userID = await getHashCache(userID);
|
userID = await getHashCache(userID);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", 'SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -1 AND "shadowHidden" != 1 ', [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, userID]);
|
const row = await db.prepare("get", 'SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -1 AND "shadowHidden" != 1 ', [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, userID], { useReplica: true });
|
||||||
|
|
||||||
if (row.minutesSaved != null) {
|
if (row.minutesSaved != null) {
|
||||||
return res.send({
|
return res.send({
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, service:
|
|||||||
|
|
||||||
const service = getService(req?.query?.service as string);
|
const service = getService(req?.query?.service as string);
|
||||||
const fetchData = () => privateDB.prepare("all", 'SELECT "hashedIP" FROM "sponsorTimes" WHERE "videoID" = ? AND "timeSubmitted" = ? AND "service" = ?',
|
const fetchData = () => privateDB.prepare("all", 'SELECT "hashedIP" FROM "sponsorTimes" WHERE "videoID" = ? AND "timeSubmitted" = ? AND "service" = ?',
|
||||||
[videoID, segment.timeSubmitted, service]) as Promise<{ hashedIP: HashedIP }[]>;
|
[videoID, segment.timeSubmitted, service], { useReplica: true }) as Promise<{ hashedIP: HashedIP }[]>;
|
||||||
cache.shadowHiddenSegmentIPs[videoID][segment.timeSubmitted] = await QueryCacher.get(fetchData, shadowHiddenIPKey(videoID, segment.timeSubmitted, service));
|
cache.shadowHiddenSegmentIPs[videoID][segment.timeSubmitted] = await QueryCacher.get(fetchData, shadowHiddenIPKey(videoID, segment.timeSubmitted, service));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +175,8 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service
|
|||||||
"all",
|
"all",
|
||||||
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted", "description" FROM "sponsorTimes"
|
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted", "description" FROM "sponsorTimes"
|
||||||
WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "startTime"`,
|
WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "startTime"`,
|
||||||
[`${hashedVideoIDPrefix}%`, service]
|
[`${hashedVideoIDPrefix}%`, service],
|
||||||
|
{ useReplica: true }
|
||||||
) as Promise<DBSegment[]>;
|
) as Promise<DBSegment[]>;
|
||||||
|
|
||||||
if (hashedVideoIDPrefix.length === 4) {
|
if (hashedVideoIDPrefix.length === 4) {
|
||||||
@@ -191,7 +192,8 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P
|
|||||||
"all",
|
"all",
|
||||||
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "timeSubmitted", "description" FROM "sponsorTimes"
|
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "timeSubmitted", "description" FROM "sponsorTimes"
|
||||||
WHERE "videoID" = ? AND "service" = ? ORDER BY "startTime"`,
|
WHERE "videoID" = ? AND "service" = ? ORDER BY "startTime"`,
|
||||||
[videoID, service]
|
[videoID, service],
|
||||||
|
{ useReplica: true }
|
||||||
) as Promise<DBSegment[]>;
|
) as Promise<DBSegment[]>;
|
||||||
|
|
||||||
return await QueryCacher.get(fetchFromDB, skipSegmentsKey(videoID, service));
|
return await QueryCacher.get(fetchFromDB, skipSegmentsKey(videoID, service));
|
||||||
@@ -283,7 +285,7 @@ async function chooseSegments(videoID: VideoID, service: Service, segments: DBSe
|
|||||||
//Segments with less than -1 votes are already ignored before this function is called
|
//Segments with less than -1 votes are already ignored before this function is called
|
||||||
async function buildSegmentGroups(segments: DBSegment[]): Promise<OverlappingSegmentGroup[]> {
|
async function buildSegmentGroups(segments: DBSegment[]): Promise<OverlappingSegmentGroup[]> {
|
||||||
const reputationPromises = segments.map(segment =>
|
const reputationPromises = segments.map(segment =>
|
||||||
segment.userID ? getReputation(segment.userID) : null);
|
segment.userID ? getReputation(segment.userID).catch((e) => Logger.error(e)) : null);
|
||||||
|
|
||||||
//Create groups of segments that are similar to eachother
|
//Create groups of segments that are similar to eachother
|
||||||
//Segments must be sorted by their startTime so that we can build groups chronologically:
|
//Segments must be sorted by their startTime so that we can build groups chronologically:
|
||||||
@@ -306,7 +308,7 @@ async function buildSegmentGroups(segments: DBSegment[]): Promise<OverlappingSeg
|
|||||||
currentGroup.votes += segment.votes;
|
currentGroup.votes += segment.votes;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segment.userID) segment.reputation = Math.min(segment.reputation, await reputationPromises[i]);
|
if (segment.userID) segment.reputation = Math.min(segment.reputation, (await reputationPromises[i]) || Infinity);
|
||||||
if (segment.reputation > 0) {
|
if (segment.reputation > 0) {
|
||||||
currentGroup.reputation += segment.reputation;
|
currentGroup.reputation += segment.reputation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
|
|||||||
const row = await db.prepare("get",
|
const row = await db.prepare("get",
|
||||||
`SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved",
|
`SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved",
|
||||||
count(*) as "segmentCount" FROM "sponsorTimes"
|
count(*) as "segmentCount" FROM "sponsorTimes"
|
||||||
WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [maxRewardTime, maxRewardTime, userID]);
|
WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [maxRewardTime, maxRewardTime, userID], { useReplica: true });
|
||||||
if (row.minutesSaved != null) {
|
if (row.minutesSaved != null) {
|
||||||
return {
|
return {
|
||||||
minutesSaved: row.minutesSaved,
|
minutesSaved: row.minutesSaved,
|
||||||
@@ -34,7 +34,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
|
|||||||
|
|
||||||
async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise<number> {
|
async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID]);
|
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
|
||||||
return row?.ignoredSegmentCount ?? 0;
|
return row?.ignoredSegmentCount ?? 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
@@ -52,7 +52,7 @@ async function dbGetUsername(userID: HashedUserID) {
|
|||||||
|
|
||||||
async function dbGetViewsForUser(userID: HashedUserID) {
|
async function dbGetViewsForUser(userID: HashedUserID) {
|
||||||
try {
|
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], { useReplica: true });
|
||||||
return row?.viewCount ?? 0;
|
return row?.viewCount ?? 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -61,7 +61,7 @@ async function dbGetViewsForUser(userID: HashedUserID) {
|
|||||||
|
|
||||||
async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
|
async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
|
||||||
try {
|
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], { useReplica: true });
|
||||||
return row?.ignoredViewCount ?? 0;
|
return row?.ignoredViewCount ?? 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
@@ -70,7 +70,7 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
|
|||||||
|
|
||||||
async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
|
async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
|
||||||
try {
|
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], { useReplica: true });
|
||||||
return row?.total ?? 0;
|
return row?.total ?? 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
|
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
|
||||||
@@ -80,7 +80,7 @@ async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
|
|||||||
|
|
||||||
async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUID> {
|
async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUID> {
|
||||||
try {
|
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], { useReplica: true });
|
||||||
return row?.UUID ?? null;
|
return row?.UUID ?? null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
@@ -89,7 +89,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUI
|
|||||||
|
|
||||||
async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<string> {
|
async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID]);
|
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID], { useReplica: true });
|
||||||
return row?.reason ?? "";
|
return row?.reason ?? "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
|
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
|
||||||
@@ -99,7 +99,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<st
|
|||||||
|
|
||||||
async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
|
async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]);
|
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true });
|
||||||
return row?.userCount > 0 ?? false;
|
return row?.userCount > 0 ?? false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function getUsername(req: Request, res: Response): Promise<Response
|
|||||||
userID = await getHashCache(userID);
|
userID = await getHashCache(userID);
|
||||||
|
|
||||||
try {
|
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], { useReplica: true });
|
||||||
|
|
||||||
if (row !== undefined) {
|
if (row !== undefined) {
|
||||||
return res.send({
|
return res.send({
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise<Resp
|
|||||||
userID = await getHashCache(userID);
|
userID = await getHashCache(userID);
|
||||||
|
|
||||||
try {
|
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], { useReplica: true });
|
||||||
|
|
||||||
//increase the view count by one
|
//increase the view count by one
|
||||||
if (row.viewCount != null) {
|
if (row.viewCount != null) {
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export async function postWarning(req: Request, res: Response): Promise<Response
|
|||||||
// check if warning is still within issue time and warning is not enabled
|
// check if warning is still within issue time and warning is not enabled
|
||||||
} else if (checkExpiredWarning(previousWarning) ) {
|
} else if (checkExpiredWarning(previousWarning) ) {
|
||||||
await db.prepare(
|
await db.prepare(
|
||||||
"run", 'UPDATE "warnings" SET "enabled" = 1 WHERE "userID" = ? AND "issueTime" = ?',
|
"run", 'UPDATE "warnings" SET "enabled" = 1, "reason" = ? WHERE "userID" = ? AND "issueTime" = ?',
|
||||||
[userID, previousWarning.issueTime]
|
[reason, userID, previousWarning.issueTime]
|
||||||
);
|
);
|
||||||
resultStatus = "re-enabled";
|
resultStatus = "re-enabled";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ async function sendWebhooks(voteData: VoteData) {
|
|||||||
async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, isTempVIP: boolean, isOwnSubmission: boolean, category: Category
|
async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, isTempVIP: boolean, isOwnSubmission: boolean, category: Category
|
||||||
, hashedIP: HashedIP, finalResponse: FinalResponse): Promise<{ status: number, message?: string }> {
|
, hashedIP: HashedIP, finalResponse: FinalResponse): Promise<{ status: number, message?: string }> {
|
||||||
// Check if they've already made a vote
|
// 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], { useReplica: true });
|
||||||
|
|
||||||
if (usersLastVoteInfo?.category === category) {
|
if (usersLastVoteInfo?.category === category) {
|
||||||
// Double vote, ignore
|
// Double vote, ignore
|
||||||
@@ -219,7 +219,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
const segmentInfo = (await db.prepare("get", `SELECT "category", "actionType", "videoID", "hashedVideoID", "service", "userID", "locked" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
const segmentInfo = (await db.prepare("get", `SELECT "category", "actionType", "videoID", "hashedVideoID", "service", "userID", "locked" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
||||||
[UUID])) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
|
[UUID], { useReplica: true })) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
|
||||||
|
|
||||||
if (segmentInfo.actionType === ActionType.Full) {
|
if (segmentInfo.actionType === ActionType.Full) {
|
||||||
return { status: 400, message: "Not allowed to change category of a full video segment" };
|
return { status: 400, message: "Not allowed to change category of a full video segment" };
|
||||||
@@ -232,7 +232,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ignore vote if the next category is locked
|
// Ignore vote if the next category is locked
|
||||||
const nextCategoryLocked = await db.prepare("get", `SELECT "videoID", "category" FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?`, [segmentInfo.videoID, segmentInfo.service, category]);
|
const nextCategoryLocked = await db.prepare("get", `SELECT "videoID", "category" FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?`, [segmentInfo.videoID, segmentInfo.service, category], { useReplica: true });
|
||||||
if (nextCategoryLocked && !isVIP) {
|
if (nextCategoryLocked && !isVIP) {
|
||||||
return { status: 200 };
|
return { status: 200 };
|
||||||
}
|
}
|
||||||
@@ -242,12 +242,13 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||||||
return { status: 200 };
|
return { status: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category]);
|
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category], { useReplica: true });
|
||||||
|
|
||||||
const timeSubmitted = Date.now();
|
const timeSubmitted = Date.now();
|
||||||
|
|
||||||
const voteAmount = (isVIP || isTempVIP) ? 500 : 1;
|
const voteAmount = (isVIP || isTempVIP) ? 500 : 1;
|
||||||
const ableToVote = isVIP || isTempVIP || finalResponse.finalStatus === 200 || true;
|
const ableToVote = finalResponse.finalStatus === 200
|
||||||
|
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID], { useReplica: true })) === undefined;
|
||||||
|
|
||||||
if (ableToVote) {
|
if (ableToVote) {
|
||||||
// Add the vote
|
// Add the vote
|
||||||
@@ -270,15 +271,15 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// See if the submissions category is ready to change
|
// See if the submissions category is ready to change
|
||||||
const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, segmentInfo.category]);
|
const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, segmentInfo.category], { useReplica: true });
|
||||||
|
|
||||||
const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID], { useReplica: true });
|
||||||
const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
|
const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
|
||||||
const startingVotes = isSubmissionVIP ? 10000 : 1;
|
const startingVotes = isSubmissionVIP ? 10000 : 1;
|
||||||
|
|
||||||
// Change this value from 1 in the future to make it harder to change categories
|
// Change this value from 1 in the future to make it harder to change categories
|
||||||
// Done this way without ORs incase the value is zero
|
// Done this way without ORs incase the value is zero
|
||||||
const currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
|
const currentCategoryCount = currentCategoryInfo?.votes ?? startingVotes;
|
||||||
|
|
||||||
// Add submission as vote
|
// Add submission as vote
|
||||||
if (!currentCategoryInfo && submissionInfo) {
|
if (!currentCategoryInfo && submissionInfo) {
|
||||||
@@ -290,7 +291,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
|||||||
|
|
||||||
//TODO: In the future, raise this number from zero to make it harder to change categories
|
//TODO: In the future, raise this number from zero to make it harder to change categories
|
||||||
// VIPs change it every time
|
// VIPs change it every time
|
||||||
if (nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2) || isVIP || isTempVIP || isOwnSubmission) {
|
if (isVIP || isTempVIP || isOwnSubmission || nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2)) {
|
||||||
// Replace the category
|
// 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]);
|
||||||
}
|
}
|
||||||
@@ -367,6 +368,19 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
|||||||
return { status: 400 };
|
return { status: 400 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MILLISECONDS_IN_HOUR = 3600000;
|
||||||
|
const now = Date.now();
|
||||||
|
const warnings = (await db.prepare("all", `SELECT "reason" FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
|
||||||
|
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
|
||||||
|
));
|
||||||
|
|
||||||
|
if (warnings.length >= config.maxNumberOfActiveWarnings) {
|
||||||
|
const warningReason = warnings[0]?.reason;
|
||||||
|
return { status: 403, message: "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?" +
|
||||||
|
`${(warningReason.length > 0 ? ` Warning reason: '${warningReason}'` : "")}` };
|
||||||
|
}
|
||||||
|
|
||||||
// no type but has category, categoryVote
|
// no type but has category, categoryVote
|
||||||
if (!type && category) {
|
if (!type && category) {
|
||||||
return categoryVote(UUID, nonAnonUserID, isVIP, isTempVIP, isOwnSubmission, category, hashedIP, finalResponse);
|
return categoryVote(UUID, nonAnonUserID, isVIP, isTempVIP, isOwnSubmission, category, hashedIP, finalResponse);
|
||||||
@@ -377,7 +391,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
|||||||
const isSegmentLocked = segmentInfo.locked;
|
const isSegmentLocked = segmentInfo.locked;
|
||||||
const isVideoLocked = async () => !!(await db.prepare("get", `SELECT "category" FROM "lockCategories" WHERE
|
const isVideoLocked = async () => !!(await db.prepare("get", `SELECT "category" FROM "lockCategories" WHERE
|
||||||
"videoID" = ? AND "service" = ? AND "category" = ? AND "actionType" = ?`,
|
"videoID" = ? AND "service" = ? AND "category" = ? AND "actionType" = ?`,
|
||||||
[segmentInfo.videoID, segmentInfo.service, segmentInfo.category, segmentInfo.actionType]));
|
[segmentInfo.videoID, segmentInfo.service, segmentInfo.category, segmentInfo.actionType], { useReplica: true }));
|
||||||
if (isSegmentLocked || await isVideoLocked()) {
|
if (isSegmentLocked || await isVideoLocked()) {
|
||||||
finalResponse.blockVote = true;
|
finalResponse.blockVote = true;
|
||||||
finalResponse.webhookType = VoteWebhookType.Rejected;
|
finalResponse.webhookType = VoteWebhookType.Rejected;
|
||||||
@@ -396,19 +410,6 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MILLISECONDS_IN_HOUR = 3600000;
|
|
||||||
const now = Date.now();
|
|
||||||
const warnings = (await db.prepare("all", `SELECT "reason" FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
|
|
||||||
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
|
|
||||||
));
|
|
||||||
|
|
||||||
if (warnings.length >= config.maxNumberOfActiveWarnings) {
|
|
||||||
const warningReason = warnings[0]?.reason;
|
|
||||||
return { status: 403, message: "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?" +
|
|
||||||
`${(warningReason.length > 0 ? ` Warning reason: '${warningReason}'` : "")}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
|
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
|
||||||
|
|
||||||
// no restrictions on checkDuration
|
// no restrictions on checkDuration
|
||||||
@@ -419,7 +420,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// check if vote has already happened
|
// 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], { useReplica: true });
|
||||||
|
|
||||||
// -1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
|
// -1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
|
||||||
// oldIncrementAmount will be zero if row is null
|
// oldIncrementAmount will be zero if row is null
|
||||||
@@ -477,9 +478,9 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
|||||||
&& !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter)
|
&& !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter)
|
||||||
&& !finalResponse.blockVote
|
&& !finalResponse.blockVote
|
||||||
&& finalResponse.finalStatus === 200
|
&& finalResponse.finalStatus === 200
|
||||||
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined
|
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) !== undefined
|
||||||
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID])) === undefined
|
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) === undefined
|
||||||
&& (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID])) === undefined);
|
&& (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID], { useReplica: true })) === undefined);
|
||||||
|
|
||||||
|
|
||||||
const ableToVote = isVIP || isTempVIP || userAbleToVote;
|
const ableToVote = isVIP || isTempVIP || userAbleToVote;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { userFeatureKey } from "./redisKeys";
|
|||||||
|
|
||||||
export async function hasFeature(userID: HashedUserID, feature: Feature): Promise<boolean> {
|
export async function hasFeature(userID: HashedUserID, feature: Feature): Promise<boolean> {
|
||||||
return await QueryCacher.get(async () => {
|
return await QueryCacher.get(async () => {
|
||||||
const result = await db.prepare("get", 'SELECT "feature" from "userFeatures" WHERE "userID" = ? AND "feature" = ?', [userID, feature]);
|
const result = await db.prepare("get", 'SELECT "feature" from "userFeatures" WHERE "userID" = ? AND "feature" = ?', [userID, feature], { useReplica: true });
|
||||||
return !!result;
|
return !!result;
|
||||||
}, userFeatureKey(userID, feature));
|
}, userFeatureKey(userID, feature));
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,6 @@ import { db } from "../databases/databases";
|
|||||||
import { HashedUserID } from "../types/user.model";
|
import { HashedUserID } from "../types/user.model";
|
||||||
|
|
||||||
export async function isUserVIP(userID: HashedUserID): Promise<boolean> {
|
export async function isUserVIP(userID: HashedUserID): Promise<boolean> {
|
||||||
return (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ? LIMIT 1`, [userID]))?.userCount > 0;
|
return (await db.prepare("get", `SELECT count(*) as "userCount" FROM "vipUsers" WHERE "userID" = ? LIMIT 1`,
|
||||||
|
[userID], { useReplica: true }))?.userCount > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export async function getReputation(userID: UserID): Promise<number> {
|
|||||||
SELECT * FROM "lockCategories" as l
|
SELECT * FROM "lockCategories" as l
|
||||||
WHERE l."videoID" = "a"."videoID" AND l."service" = "a"."service" AND l."category" = "a"."category" LIMIT 1)
|
WHERE l."videoID" = "a"."videoID" AND l."service" = "a"."service" AND l."category" = "a"."category" LIMIT 1)
|
||||||
THEN 1 ELSE 0 END) AS "mostUpvotedInLockedVideoSum"
|
THEN 1 ELSE 0 END) AS "mostUpvotedInLockedVideoSum"
|
||||||
FROM "sponsorTimes" as "a" WHERE "userID" = ?`, [userID, weekAgo, pastDate, userID]) as Promise<ReputationDBResult>;
|
FROM "sponsorTimes" as "a" WHERE "userID" = ?`, [userID, weekAgo, pastDate, userID], { useReplica: true }) as Promise<ReputationDBResult>;
|
||||||
|
|
||||||
const result = await QueryCacher.get(fetchFromDB, reputationKey(userID));
|
const result = await QueryCacher.get(fetchFromDB, reputationKey(userID));
|
||||||
|
|
||||||
@@ -55,6 +55,8 @@ function convertRange(value: number, currentMin: number, currentMax: number, tar
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function calculateReputationFromMetrics(metrics: ReputationDBResult): number {
|
export function calculateReputationFromMetrics(metrics: ReputationDBResult): number {
|
||||||
|
if (!metrics) return 0;
|
||||||
|
|
||||||
// Grace period
|
// Grace period
|
||||||
if (metrics.totalSubmissions < 5) {
|
if (metrics.totalSubmissions < 5) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ describe("voteOnSponsorTime", () => {
|
|||||||
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-1", 8, 12, 0, 1, "category-change-uuid-6", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-1", 8, 12, 0, 1, "category-change-uuid-6", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
||||||
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-1", 9, 14, 0, 0, "category-change-uuid-7", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-1", 9, 14, 0, 0, "category-change-uuid-7", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
||||||
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-1", 7, 12, 0, 1, "category-change-uuid-8", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-1", 7, 12, 0, 1, "category-change-uuid-8", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
||||||
|
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-2", 7, 14, 0, 0, "category-warnvote-uuid-0", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
||||||
|
await db.prepare("run", insertSponsorTimeQuery, ["category-change-test-2", 8, 13, 0, 0, "category-banvote-uuid-0", categoryChangeUserHash, 0, 50, "intro", "skip", 0, 0]);
|
||||||
await db.prepare("run", insertSponsorTimeQuery, ["duration-update", 1, 10, 0, 0, "duration-update-uuid-1", "testman", 0, 0, "intro", "skip", 0, 0]);
|
await db.prepare("run", insertSponsorTimeQuery, ["duration-update", 1, 10, 0, 0, "duration-update-uuid-1", "testman", 0, 0, "intro", "skip", 0, 0]);
|
||||||
await db.prepare("run", insertSponsorTimeQuery, ["full-video", 1, 10, 0, 0, "full-video-uuid-1", "testman", 0, 0, "sponsor", "full", 0, 0]);
|
await db.prepare("run", insertSponsorTimeQuery, ["full-video", 1, 10, 0, 0, "full-video-uuid-1", "testman", 0, 0, "sponsor", "full", 0, 0]);
|
||||||
// videoDuration change
|
// videoDuration change
|
||||||
@@ -445,6 +447,32 @@ describe("voteOnSponsorTime", () => {
|
|||||||
.catch(err => done(err));
|
.catch(err => done(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should not be able to vote for a category of a segment (Too many warning)", (done) => {
|
||||||
|
const UUID = "category-warnvote-uuid-0";
|
||||||
|
const category = "preview";
|
||||||
|
postVoteCategory("warn-voteuser01", UUID, category)
|
||||||
|
.then(res => {
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should be able to vote for a category as a shadowbanned user, but it shouldn't add your vote to the database", (done) => {
|
||||||
|
const UUID = "category-banvote-uuid-0";
|
||||||
|
const category = "preview";
|
||||||
|
postVoteCategory("randomID4", UUID, category)
|
||||||
|
.then(async res => {
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const row = await getSegmentCategory(UUID);
|
||||||
|
const categoryRows = await db.prepare("all", `SELECT votes, category FROM "categoryVotes" WHERE "UUID" = ?`, [UUID]);
|
||||||
|
assert.strictEqual(row.category, "intro");
|
||||||
|
assert.strictEqual(categoryRows.length, 0);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(err => done(err));
|
||||||
|
});
|
||||||
|
|
||||||
it("Should not be able to category-vote on an invalid UUID submission", (done) => {
|
it("Should not be able to category-vote on an invalid UUID submission", (done) => {
|
||||||
const UUID = "invalid-uuid";
|
const UUID = "invalid-uuid";
|
||||||
postVoteCategory("randomID3", UUID, "intro")
|
postVoteCategory("randomID3", UUID, "intro")
|
||||||
|
|||||||
Reference in New Issue
Block a user