mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-15 16:07:03 +03:00
Add ability to add manually choose who can submit chapters
This commit is contained in:
@@ -48,6 +48,7 @@ import { getRating } from "./routes/ratings/getRating";
|
||||
import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache";
|
||||
import { getTopCategoryUsers } from "./routes/getTopCategoryUsers";
|
||||
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
|
||||
import { addFeature } from "./routes/addFeature";
|
||||
|
||||
export function createServer(callback: () => void): Server {
|
||||
// Create a service (the app object is just a callback).
|
||||
@@ -196,6 +197,8 @@ function setupRoutes(router: Router) {
|
||||
|
||||
router.get("/api/lockReason", getLockReason);
|
||||
|
||||
router.post("/api/feature", addFeature)
|
||||
|
||||
// ratings
|
||||
router.get("/api/ratings/rate/:prefix", getRating);
|
||||
router.get("/api/ratings/rate", getRating);
|
||||
|
||||
72
src/routes/addFeature.ts
Normal file
72
src/routes/addFeature.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { getHashCache } from "../utils/getHashCache";
|
||||
import { db } from "../databases/databases";
|
||||
import { config } from "../config";
|
||||
import { Request, Response } from "express";
|
||||
import { isUserVIP } from "../utils/isUserVIP";
|
||||
import { Feature, HashedUserID } from "../types/user.model";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
|
||||
interface AddFeatureRequest extends Request {
|
||||
body: {
|
||||
userID: HashedUserID;
|
||||
adminUserID: string;
|
||||
feature: string;
|
||||
enabled: string;
|
||||
}
|
||||
}
|
||||
|
||||
const allowedFeatures = {
|
||||
vip: [
|
||||
Feature.ChapterSubmitter
|
||||
],
|
||||
admin: [
|
||||
Feature.ChapterSubmitter
|
||||
]
|
||||
}
|
||||
|
||||
export async function addFeature(req: AddFeatureRequest, res: Response): Promise<Response> {
|
||||
const { body: { userID, adminUserID } } = req;
|
||||
const feature = parseInt(req.body.feature) as Feature;
|
||||
const enabled = req.body?.enabled !== "false";
|
||||
|
||||
if (!userID || !adminUserID) {
|
||||
// invalid request
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
// hash the userID
|
||||
const adminUserIDInput = await getHashCache(adminUserID);
|
||||
const isAdmin = adminUserIDInput !== config.adminUserID;
|
||||
const isVIP = (await isUserVIP(userID)) || isAdmin;
|
||||
|
||||
if (!isAdmin && !isVIP) {
|
||||
// not authorized
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
try {
|
||||
const currentAllowedFeatures = isAdmin ? allowedFeatures.admin : allowedFeatures.vip;
|
||||
if (currentAllowedFeatures.includes(feature)) {
|
||||
if (enabled) {
|
||||
const featureAdded = await db.prepare("get", 'SELECT "feature" from "userFeatures" WHERE "userID" = ? AND "feature" = ?', [userID, feature]);
|
||||
if (!featureAdded) {
|
||||
await db.prepare("run", 'INSERT INTO "userFeatures" ("userID", "feature", "issuerUserID", "timeSubmitted") VALUES(?, ?, ?, ?)'
|
||||
, [userID, feature, adminUserID, Date.now()]);
|
||||
}
|
||||
} else {
|
||||
await db.prepare("run", 'DELETE FROM "userFeatures" WHERE "userID" = ? AND "feature" = ?', [userID, feature]);
|
||||
}
|
||||
|
||||
QueryCacher.clearFeatureCache(userID, feature);
|
||||
} else {
|
||||
return res.status(400).send("Invalid feature");
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
} catch (e) {
|
||||
Logger.error(e as string);
|
||||
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { getReputation } from "../utils/reputation";
|
||||
import { SegmentUUID } from "../types/segments.model";
|
||||
import { config } from "../config";
|
||||
import { canSubmitChapter } from "../utils/permissions";
|
||||
const maxRewardTime = config.maxRewardTimePerSegmentInSeconds;
|
||||
|
||||
async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ minutesSaved: number, segmentCount: number }> {
|
||||
@@ -105,10 +106,6 @@ async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function dbCanSubmitChapter(userID: HashedUserID): Promise<boolean> {
|
||||
return (await isUserVIP(userID)) || (await getReputation(userID)) > config.minReputationToSubmitChapter;
|
||||
}
|
||||
|
||||
type cases = Record<string, any>
|
||||
|
||||
const executeIfFunction = (f: any) =>
|
||||
@@ -133,7 +130,7 @@ const dbGetValue = (userID: HashedUserID, property: string): Promise<string|Segm
|
||||
reputation: () => getReputation(userID),
|
||||
vip: () => isUserVIP(userID),
|
||||
lastSegmentID: () => dbGetLastSegmentForUser(userID),
|
||||
canSubmitChapter: () => dbCanSubmitChapter(userID)
|
||||
canSubmitChapter: () => canSubmitChapter(userID)
|
||||
})("")(property);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { parseUserAgent } from "../utils/userAgent";
|
||||
import { getService } from "../utils/getService";
|
||||
import axios from "axios";
|
||||
import { vote } from "./voteOnSponsorTime";
|
||||
import { canSubmitChapter } from "../utils/permissions";
|
||||
|
||||
type CheckResult = {
|
||||
pass: boolean,
|
||||
@@ -200,7 +201,8 @@ async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
|
||||
return CHECK_PASS;
|
||||
}
|
||||
|
||||
function checkInvalidFields(videoID: VideoID, userID: UserID, segments: IncomingSegment[]): CheckResult {
|
||||
async function checkInvalidFields(videoID: VideoID, userID: UserID, hashedUserID: HashedUserID
|
||||
, segments: IncomingSegment[]): Promise<CheckResult> {
|
||||
const invalidFields = [];
|
||||
const errors = [];
|
||||
if (typeof videoID !== "string" || videoID?.length == 0) {
|
||||
@@ -227,6 +229,10 @@ function checkInvalidFields(videoID: VideoID, userID: UserID, segments: Incoming
|
||||
|| (segmentPair.description.length !== 0 && segmentPair.actionType !== ActionType.Chapter)) {
|
||||
invalidFields.push("segment description");
|
||||
}
|
||||
|
||||
if (segmentPair.actionType === ActionType.Chapter && !(await canSubmitChapter(hashedUserID))) {
|
||||
invalidFields.push("permission to submit chapters");
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidFields.length !== 0) {
|
||||
@@ -478,14 +484,14 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { videoID, userID: paramUserID, service, videoDuration, videoDurationParam, segments, userAgent } = preprocessInput(req);
|
||||
|
||||
const invalidCheckResult = checkInvalidFields(videoID, paramUserID, segments);
|
||||
//hash the userID
|
||||
const userID = await getHashCache(paramUserID || "");
|
||||
|
||||
const invalidCheckResult = await checkInvalidFields(videoID, paramUserID, userID, segments);
|
||||
if (!invalidCheckResult.pass) {
|
||||
return res.status(invalidCheckResult.errorCode).send(invalidCheckResult.errorMessage);
|
||||
}
|
||||
|
||||
//hash the userID
|
||||
const userID = await getHashCache(paramUserID);
|
||||
|
||||
const userWarningCheckResult = await checkUserActiveWarning(userID);
|
||||
if (!userWarningCheckResult.pass) {
|
||||
Logger.warn(`Caught a submission for a warned user. userID: '${userID}', videoID: '${videoID}', category: '${segments.reduce<string>((prev, val) => `${prev} ${val.category}`, "")}', times: ${segments.reduce<string>((prev, val) => `${prev} ${val.segment}`, "")}`);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { HashedValue } from "./hash.model";
|
||||
|
||||
export type UserID = string & { __userIDBrand: unknown };
|
||||
export type HashedUserID = UserID & HashedValue;
|
||||
export type HashedUserID = UserID & HashedValue;
|
||||
|
||||
export enum Feature {
|
||||
ChapterSubmitter = 0
|
||||
}
|
||||
11
src/utils/features.ts
Normal file
11
src/utils/features.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { db } from "../databases/databases";
|
||||
import { Feature, HashedUserID } from "../types/user.model";
|
||||
import { QueryCacher } from "./queryCacher";
|
||||
import { userFeatureKey } from "./redisKeys";
|
||||
|
||||
export async function hasFeature(userID: HashedUserID, feature: Feature): Promise<boolean> {
|
||||
return await QueryCacher.get(async () => {
|
||||
const result = await db.prepare("get", 'SELECT "feature" from "userFeatures" WHERE "userID" = ? AND "feature" = ?', [userID, feature]);
|
||||
return !!result;
|
||||
}, userFeatureKey(userID, feature));
|
||||
}
|
||||
11
src/utils/permissions.ts
Normal file
11
src/utils/permissions.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { config } from "../config";
|
||||
import { Feature, HashedUserID } from "../types/user.model";
|
||||
import { hasFeature } from "./features";
|
||||
import { isUserVIP } from "./isUserVIP";
|
||||
import { getReputation } from "./reputation";
|
||||
|
||||
export async function canSubmitChapter(userID: HashedUserID): Promise<boolean> {
|
||||
return (await isUserVIP(userID))
|
||||
|| (await getReputation(userID)) > config.minReputationToSubmitChapter
|
||||
|| (await hasFeature(userID, Feature.ChapterSubmitter));
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import redis from "../utils/redis";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey } from "./redisKeys";
|
||||
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey } from "./redisKeys";
|
||||
import { Service, VideoID, VideoIDHash } from "../types/segments.model";
|
||||
import { UserID } from "../types/user.model";
|
||||
import { Feature, HashedUserID, UserID } from "../types/user.model";
|
||||
|
||||
async function get<T>(fetchFromDB: () => Promise<T>, key: string): Promise<T> {
|
||||
try {
|
||||
@@ -90,9 +90,14 @@ function clearRatingCache(videoInfo: { hashedVideoID: VideoIDHash; service: Serv
|
||||
}
|
||||
}
|
||||
|
||||
function clearFeatureCache(userID: HashedUserID, feature: Feature): void {
|
||||
redis.del(userFeatureKey(userID, feature)).catch((err) => Logger.error(err));
|
||||
}
|
||||
|
||||
export const QueryCacher = {
|
||||
get,
|
||||
getAndSplit,
|
||||
clearSegmentCache,
|
||||
clearRatingCache
|
||||
clearRatingCache,
|
||||
clearFeatureCache
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Service, VideoID, VideoIDHash } from "../types/segments.model";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { Feature, HashedUserID, UserID } from "../types/user.model";
|
||||
import { HashedValue } from "../types/hash.model";
|
||||
import { Logger } from "./logger";
|
||||
|
||||
@@ -36,4 +36,8 @@ export function shaHashKey(singleIter: HashedValue): string {
|
||||
}
|
||||
|
||||
export const tempVIPKey = (userID: HashedUserID): string =>
|
||||
`vip.temp.${userID}`;
|
||||
`vip.temp.${userID}`;
|
||||
|
||||
export function userFeatureKey (userID: HashedUserID, feature: Feature): string {
|
||||
return `user.${userID}.feature.${feature}`;
|
||||
}
|
||||
Reference in New Issue
Block a user