mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-17 13:08:49 +03:00
create an isUserBanned utility function
This commit is contained in:
@@ -8,12 +8,12 @@ import { getReputation } from "../utils/reputation";
|
||||
import { Category, SegmentUUID } from "../types/segments.model";
|
||||
import { config } from "../config";
|
||||
import { canSubmit } from "../utils/permissions";
|
||||
import { isUserBanned } from "../utils/checkBan";
|
||||
const maxRewardTime = config.maxRewardTimePerSegmentInSeconds;
|
||||
|
||||
async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ minutesSaved: number, segmentCount: number }> {
|
||||
try {
|
||||
const userBanCount = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]))?.userCount;
|
||||
const countShadowHidden = userBanCount > 0 ? 2 : 1; // if shadowbanned, count shadowhidden as well
|
||||
const countShadowHidden = await isUserBanned(userID) ? 2 : 1; // if shadowbanned, count shadowhidden as well
|
||||
const row = await db.prepare("get",
|
||||
`SELECT SUM(CASE WHEN "actionType" = 'chapter' THEN 0 ELSE ((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views" END) as "minutesSaved",
|
||||
count(*) as "segmentCount" FROM "sponsorTimes"
|
||||
@@ -111,8 +111,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<st
|
||||
|
||||
async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
|
||||
try {
|
||||
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 await isUserBanned(userID);
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Request, Response } from "express";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { config } from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { isUserBanned } from "../utils/checkBan";
|
||||
type nestedObj = Record<string, Record<string, number>>;
|
||||
const maxRewardTimePerSegmentInSeconds = config.maxRewardTimePerSegmentInSeconds ?? 86400;
|
||||
|
||||
@@ -34,8 +35,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea
|
||||
`;
|
||||
}
|
||||
try {
|
||||
const userBanCount = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]))?.userCount;
|
||||
const countShadowHidden = userBanCount > 0 ? 2 : 1; // if shadowbanned, count shadowhidden as well
|
||||
const countShadowHidden = await isUserBanned(userID) ? 2 : 1; // if shadowbanned, count shadowhidden as well
|
||||
const row = await db.prepare("get", `
|
||||
SELECT SUM(CASE WHEN "actionType" = 'chapter' THEN 0 ELSE ((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views" END) as "minutesSaved",
|
||||
${additionalQuery}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getIP } from "../utils/getIP";
|
||||
import { getFormattedTime } from "../utils/getFormattedTime";
|
||||
import { dispatchEvent } from "../utils/webhookUtils";
|
||||
import { Request, Response } from "express";
|
||||
import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model";
|
||||
import { ActionType, Category, HashedIP, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model";
|
||||
import { deleteLockCategories } from "./deleteLockCategories";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import { getReputation } from "../utils/reputation";
|
||||
@@ -23,8 +23,8 @@ import { vote } from "./voteOnSponsorTime";
|
||||
import { canSubmit } from "../utils/permissions";
|
||||
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
|
||||
import * as youtubeID from "../utils/youtubeID";
|
||||
import { banUser } from "./shadowBanUser";
|
||||
import { acquireLock } from "../utils/redisLock";
|
||||
import { checkBanStatus } from "../utils/checkBan";
|
||||
|
||||
type CheckResult = {
|
||||
pass: boolean,
|
||||
@@ -544,7 +544,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
const newSegments = [];
|
||||
|
||||
//hash the ip 5000 times so no one can get it from the database
|
||||
const hashedIP = await getHashCache(rawIP + config.globalSalt);
|
||||
const hashedIP = await getHashCache(rawIP + config.globalSalt) as HashedIP;
|
||||
|
||||
const timeSubmitted = Date.now();
|
||||
|
||||
@@ -554,22 +554,14 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
// }
|
||||
|
||||
//check to see if this user is shadowbanned
|
||||
const userBanCount = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]))?.userCount;
|
||||
const ipBanCount = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedIPs" WHERE "hashedIP" = ? LIMIT 1`, [hashedIP]))?.userCount;
|
||||
const shadowBanCount = userBanCount || ipBanCount;
|
||||
const isBanned = await checkBanStatus(userID, hashedIP);
|
||||
const startingVotes = 0;
|
||||
const reputation = await getReputation(userID);
|
||||
|
||||
if (!userBanCount && ipBanCount) {
|
||||
// Make sure the whole user is banned
|
||||
banUser(userID, true, true, 1, config.categoryList as Category[], config.deArrowTypes)
|
||||
.catch((e) => Logger.error(`Error banning user after submitting from a banned IP: ${e}`));
|
||||
}
|
||||
|
||||
for (const segmentInfo of segments) {
|
||||
// Full segments are always rejected since there can only be one, so shadow hide wouldn't work
|
||||
if (segmentInfo.ignoreSegment
|
||||
|| (shadowBanCount && segmentInfo.actionType === ActionType.Full)) {
|
||||
|| (isBanned && segmentInfo.actionType === ActionType.Full)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -586,7 +578,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
|
||||
("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "actionType", "service", "videoDuration", "reputation", "shadowHidden", "hashedVideoID", "userAgent", "description")
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
||||
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, startingLocked, UUID, userID, timeSubmitted, 0
|
||||
, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, shadowBanCount, hashedVideoID, userAgent, segmentInfo.description
|
||||
, segmentInfo.category, segmentInfo.actionType, service, videoDuration, reputation, isBanned ? 1 : 0, hashedVideoID, userAgent, segmentInfo.description
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Logger } from "../utils/logger";
|
||||
import { db, privateDB } from "../databases/databases";
|
||||
import { getHashCache } from "../utils/getHashCache";
|
||||
import { Request, Response } from "express";
|
||||
import { isUserBanned } from "../utils/checkBan";
|
||||
import { HashedUserID } from "../types/user.model";
|
||||
|
||||
function logUserNameChange(userID: string, newUserName: string, oldUserName: string, updatedByAdmin: boolean): Promise<Response> {
|
||||
return privateDB.prepare("run",
|
||||
@@ -12,12 +14,12 @@ function logUserNameChange(userID: string, newUserName: string, oldUserName: str
|
||||
}
|
||||
|
||||
export async function setUsername(req: Request, res: Response): Promise<Response> {
|
||||
let userID = req.query.userID as string;
|
||||
const userIDInput = req.query.userID as string;
|
||||
const adminUserIDInput = req.query.adminUserID as string;
|
||||
let userName = req.query.username as string;
|
||||
let hashedUserID: HashedUserID;
|
||||
|
||||
let adminUserIDInput = req.query.adminUserID as string;
|
||||
|
||||
if (userID == undefined || userName == undefined || userID === "undefined" || userName.length > 64) {
|
||||
if (userIDInput == undefined || userName == undefined || userIDInput === "undefined" || userName.length > 64) {
|
||||
//invalid request
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
@@ -35,41 +37,37 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
||||
userName = userName.replace(/[\u0000-\u001F\u007F-\u009F]/g, "");
|
||||
|
||||
try {
|
||||
// check privateID against publicID
|
||||
if (!await checkPrivateUsername(userName, userID)) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
if (adminUserIDInput != undefined) {
|
||||
//this is the admin controlling the other users account, don't hash the controling account's ID
|
||||
adminUserIDInput = await getHashCache(adminUserIDInput);
|
||||
hashedUserID = userIDInput as HashedUserID;
|
||||
|
||||
if (adminUserIDInput != config.adminUserID) {
|
||||
if (await getHashCache(adminUserIDInput) != config.adminUserID) {
|
||||
//they aren't the admin
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
} else {
|
||||
// check privateID against publicID
|
||||
if (!await checkPrivateUsername(userName, userIDInput)) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
//hash the userID
|
||||
userID = await getHashCache(userID);
|
||||
hashedUserID = await getHashCache(userIDInput) as HashedUserID;
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "userNames" WHERE "userID" = ? AND "locked" = 1`, [hashedUserID]);
|
||||
if (row.userCount > 0) {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
if (await isUserBanned(hashedUserID)) {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
}
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "userNames" WHERE "userID" = ? AND "locked" = 1`, [userID]);
|
||||
if (adminUserIDInput === undefined && row.userCount > 0) {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
const shadowBanRow = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]);
|
||||
if (adminUserIDInput === undefined && shadowBanRow.userCount > 0) {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
|
||||
timings.push(Date.now());
|
||||
}
|
||||
catch (error) /* istanbul ignore next */ {
|
||||
Logger.error(error as string);
|
||||
@@ -78,7 +76,7 @@ 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`, [hashedUserID]);
|
||||
const locked = adminUserIDInput === undefined ? 0 : 1;
|
||||
let oldUserName = "";
|
||||
|
||||
@@ -87,19 +85,19 @@ export async function setUsername(req: Request, res: Response): Promise<Response
|
||||
if (row?.userName !== undefined) {
|
||||
//already exists, update this row
|
||||
oldUserName = row.userName;
|
||||
if (userName == userID && !locked) {
|
||||
await db.prepare("run", `DELETE FROM "userNames" WHERE "userID" = ?`, [userID]);
|
||||
if (userName == hashedUserID && !locked) {
|
||||
await db.prepare("run", `DELETE FROM "userNames" WHERE "userID" = ?`, [hashedUserID]);
|
||||
} else {
|
||||
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, hashedUserID]);
|
||||
}
|
||||
} 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(?, ?, ?)`, [hashedUserID, userName, locked]);
|
||||
}
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
await logUserNameChange(userID, userName, oldUserName, adminUserIDInput !== undefined);
|
||||
await logUserNameChange(hashedUserID, userName, oldUserName, adminUserIDInput !== undefined);
|
||||
|
||||
timings.push(Date.now());
|
||||
|
||||
@@ -118,4 +116,4 @@ async function checkPrivateUsername(username: string, userID: string): Promise<b
|
||||
const userNameRow = await db.prepare("get", `SELECT "userID" FROM "userNames" WHERE "userID" = ? LIMIT 1`, [userNameHash]);
|
||||
if (userNameRow?.userID) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import { getFormattedTime } from "../utils/getFormattedTime";
|
||||
import { getIP } from "../utils/getIP";
|
||||
import { getHashCache } from "../utils/getHashCache";
|
||||
import { config } from "../config";
|
||||
import { UserID } from "../types/user.model";
|
||||
import { HashedUserID, UserID } from "../types/user.model";
|
||||
import { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, VideoDuration, ActionType, VoteType } from "../types/segments.model";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import axios from "axios";
|
||||
import { getVideoDetails, videoDetails } from "../utils/getVideoDetails";
|
||||
import { deleteLockCategories } from "./deleteLockCategories";
|
||||
import { acquireLock } from "../utils/redisLock";
|
||||
import { checkBanStatus } from "../utils/checkBan";
|
||||
|
||||
const voteTypes = {
|
||||
normal: 0,
|
||||
@@ -208,7 +209,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: HashedUserID, isVIP: boolean, isTempVIP: boolean, isOwnSubmission: boolean, category: Category
|
||||
, hashedIP: HashedIP, finalResponse: FinalResponse): Promise<{ status: number, message?: string }> {
|
||||
// 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], { useReplica: true });
|
||||
@@ -244,8 +245,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
const timeSubmitted = Date.now();
|
||||
|
||||
const voteAmount = (isVIP || isTempVIP) ? 500 : 1;
|
||||
const ableToVote = finalResponse.finalStatus === 200
|
||||
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID], { useReplica: true })) === undefined;
|
||||
const ableToVote = finalResponse.finalStatus === 200; // ban status checks handled by vote() (caller function)
|
||||
|
||||
if (ableToVote) {
|
||||
// Add the vote
|
||||
@@ -336,6 +336,9 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
||||
const nonAnonUserID = await getHashCache(paramUserID);
|
||||
const userID = await getHashCache(paramUserID + UUID);
|
||||
|
||||
//hash the ip 5000 times so no one can get it from the database
|
||||
const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress);
|
||||
|
||||
const lock = await acquireLock(`voteOnSponsorTime:${UUID}.${paramUserID}`);
|
||||
if (!lock.status) {
|
||||
return { status: 429, message: "Vote already in progress" };
|
||||
@@ -350,9 +353,6 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
||||
webhookMessage: null
|
||||
};
|
||||
|
||||
//hash the ip 5000 times so no one can get it from the database
|
||||
const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress);
|
||||
|
||||
const segmentInfo: DBSegment = await db.prepare("get", `SELECT * from "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
||||
// segment doesnt exist
|
||||
if (!segmentInfo) {
|
||||
@@ -362,6 +362,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
||||
|
||||
const isTempVIP = await isUserTempVIP(nonAnonUserID, segmentInfo.videoID);
|
||||
const isVIP = await isUserVIP(nonAnonUserID);
|
||||
const isBanned = await checkBanStatus(nonAnonUserID, hashedIP); // propagates IP bans
|
||||
|
||||
//check if user voting on own submission
|
||||
const isOwnSubmission = nonAnonUserID === segmentInfo.userID;
|
||||
@@ -380,11 +381,19 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
||||
|
||||
if (warnings.length >= config.maxNumberOfActiveWarnings) {
|
||||
const warningReason = warnings[0]?.reason;
|
||||
lock.unlock();
|
||||
return { status: 403, message: "Vote rejected due to a tip 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}'` : "")}` };
|
||||
}
|
||||
|
||||
// we can return out of the function early if the user is banned after warning checks
|
||||
// returning before warning checks would make them not appear on vote if the user is also banned
|
||||
if (isBanned) {
|
||||
lock.unlock();
|
||||
return { status: 200 };
|
||||
}
|
||||
|
||||
// no type but has category, categoryVote
|
||||
if (!type && category) {
|
||||
const result = categoryVote(UUID, nonAnonUserID, isVIP, isTempVIP, isOwnSubmission, category, hashedIP, finalResponse);
|
||||
@@ -486,13 +495,13 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
|
||||
}
|
||||
|
||||
// Only change the database if they have made a submission before and haven't voted recently
|
||||
// ban status check was handled earlier (w/ early return)
|
||||
const ableToVote = isVIP || isTempVIP || (
|
||||
(!(isOwnSubmission && incrementAmount > 0 && oldIncrementAmount >= 0)
|
||||
&& !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter)
|
||||
&& !finalResponse.blockVote
|
||||
&& finalResponse.finalStatus === 200
|
||||
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ? AND "category" = ? AND "votes" > -2 AND "hidden" = 0 AND "shadowHidden" = 0 LIMIT 1`, [nonAnonUserID, segmentInfo.category], { useReplica: true }) !== 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], { useReplica: true })) === undefined)
|
||||
);
|
||||
|
||||
|
||||
27
src/utils/checkBan.ts
Normal file
27
src/utils/checkBan.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HashedUserID } from "../types/user.model";
|
||||
import { db } from "../databases/databases";
|
||||
import { Category, HashedIP } from "../types/segments.model";
|
||||
import { banUser } from "../routes/shadowBanUser";
|
||||
import { config } from "../config";
|
||||
import { Logger } from "./logger";
|
||||
|
||||
export async function isUserBanned(userID: HashedUserID): Promise<boolean> {
|
||||
return (await db.prepare("get", `SELECT 1 FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true })) !== undefined;
|
||||
}
|
||||
|
||||
export async function isIPBanned(ip: HashedIP): Promise<boolean> {
|
||||
return (await db.prepare("get", `SELECT 1 FROM "shadowBannedIPs" WHERE "hashedIP" = ? LIMIT 1`, [ip], { useReplica: true })) !== undefined;
|
||||
}
|
||||
|
||||
// NOTE: this function will propagate IP bans
|
||||
export async function checkBanStatus(userID: HashedUserID, ip: HashedIP): Promise<boolean> {
|
||||
const userBanStatus = await isUserBanned(userID);
|
||||
const ipBanStatus = await isIPBanned(ip);
|
||||
|
||||
if (!userBanStatus && ipBanStatus) {
|
||||
// Make sure the whole user is banned
|
||||
banUser(userID, true, true, 1, config.categoryList as Category[], config.deArrowTypes)
|
||||
.catch((e) => Logger.error(`Error banning user after submitting from a banned IP: ${e}`));
|
||||
}
|
||||
return userBanStatus || ipBanStatus;
|
||||
}
|
||||
Reference in New Issue
Block a user