mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-13 15:06:59 +03:00
Merge pull request #558 from mini-bomba/dearrow-bans
Fix Dearrow bans + some bug fixes
This commit is contained in:
11
databases/_upgrade_sponsorTimes_38.sql
Normal file
11
databases/_upgrade_sponsorTimes_38.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
UPDATE "titleVotes" SET "shadowHidden" = 1
|
||||
WHERE "UUID" IN (SELECT "UUID" FROM "titles" INNER JOIN "shadowBannedUsers" "bans" ON "titles"."userID" = "bans"."userID");
|
||||
|
||||
UPDATE "thumbnailVotes" SET "shadowHidden" = 1
|
||||
WHERE "UUID" IN (SELECT "UUID" FROM "thumbnails" INNER JOIN "shadowBannedUsers" "bans" ON "thumbnails"."userID" = "bans"."userID");
|
||||
|
||||
UPDATE "config" SET value = 38 WHERE key = 'version';
|
||||
|
||||
COMMIT;
|
||||
@@ -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}
|
||||
|
||||
@@ -14,6 +14,7 @@ import crypto from "crypto";
|
||||
import { QueryCacher } from "../utils/queryCacher";
|
||||
import { acquireLock } from "../utils/redisLock";
|
||||
import { hasFeature } from "../utils/features";
|
||||
import { checkBanStatus } from "../utils/checkBan";
|
||||
|
||||
enum BrandingType {
|
||||
Title,
|
||||
@@ -43,6 +44,7 @@ export async function postBranding(req: Request, res: Response) {
|
||||
const isVip = await isUserVIP(hashedUserID);
|
||||
const hashedVideoID = await getHashCache(videoID, 1);
|
||||
const hashedIP = await getHashCache(getIP(req) + config.globalSalt as IPAddress);
|
||||
const isBanned = await checkBanStatus(hashedUserID, hashedIP);
|
||||
|
||||
const lock = await acquireLock(`postBranding:${videoID}.${hashedUserID}`);
|
||||
if (!lock.status) {
|
||||
@@ -61,7 +63,11 @@ export async function postBranding(req: Request, res: Response) {
|
||||
|
||||
await Promise.all([(async () => {
|
||||
if (title) {
|
||||
// ignore original submissions from banned users - hiding those would cause issues
|
||||
if (title.original && isBanned) return;
|
||||
|
||||
const existingUUID = (await db.prepare("get", `SELECT "UUID" from "titles" where "videoID" = ? AND "title" = ?`, [videoID, title.title]))?.UUID;
|
||||
if (existingUUID != undefined && isBanned) return; // ignore votes on existing details from banned users
|
||||
const UUID = existingUUID || crypto.randomUUID();
|
||||
|
||||
const existingVote = await handleExistingVotes(BrandingType.Title, videoID, hashedUserID, UUID, hashedIP, voteType);
|
||||
@@ -72,8 +78,8 @@ export async function postBranding(req: Request, res: Response) {
|
||||
[videoID, title.title, title.original ? 1 : 0, hashedUserID, service, hashedVideoID, now, UUID]);
|
||||
|
||||
const verificationValue = await getVerificationValue(hashedUserID, isVip);
|
||||
await db.prepare("run", `INSERT INTO "titleVotes" ("UUID", "votes", "locked", "shadowHidden", "verification") VALUES (?, 0, ?, 0, ?);`,
|
||||
[UUID, isVip ? 1 : 0, verificationValue]);
|
||||
await db.prepare("run", `INSERT INTO "titleVotes" ("UUID", "votes", "locked", "shadowHidden", "verification") VALUES (?, 0, ?, ?, ?);`,
|
||||
[UUID, isVip ? 1 : 0, isBanned ? 1 : 0, verificationValue]);
|
||||
|
||||
await verifyOldSubmissions(hashedUserID, verificationValue);
|
||||
}
|
||||
@@ -85,10 +91,14 @@ export async function postBranding(req: Request, res: Response) {
|
||||
}
|
||||
})(), (async () => {
|
||||
if (thumbnail) {
|
||||
// ignore original submissions from banned users - hiding those would cause issues
|
||||
if (thumbnail.original && isBanned) return;
|
||||
|
||||
const existingUUID = thumbnail.original
|
||||
? (await db.prepare("get", `SELECT "UUID" from "thumbnails" where "videoID" = ? AND "original" = 1`, [videoID]))?.UUID
|
||||
: (await db.prepare("get", `SELECT "thumbnails"."UUID" from "thumbnailTimestamps" JOIN "thumbnails" ON "thumbnails"."UUID" = "thumbnailTimestamps"."UUID"
|
||||
WHERE "thumbnailTimestamps"."timestamp" = ? AND "thumbnails"."videoID" = ?`, [(thumbnail as TimeThumbnailSubmission).timestamp, videoID]))?.UUID;
|
||||
if (existingUUID != undefined && isBanned) return; // ignore votes on existing details from banned users
|
||||
const UUID = existingUUID || crypto.randomUUID();
|
||||
|
||||
const existingVote = await handleExistingVotes(BrandingType.Thumbnail, videoID, hashedUserID, UUID, hashedIP, voteType);
|
||||
@@ -98,8 +108,8 @@ export async function postBranding(req: Request, res: Response) {
|
||||
await db.prepare("run", `INSERT INTO "thumbnails" ("videoID", "original", "userID", "service", "hashedVideoID", "timeSubmitted", "UUID") VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[videoID, thumbnail.original ? 1 : 0, hashedUserID, service, hashedVideoID, now, UUID]);
|
||||
|
||||
await db.prepare("run", `INSERT INTO "thumbnailVotes" ("UUID", "votes", "locked", "shadowHidden") VALUES (?, 0, ?, 0)`,
|
||||
[UUID, isVip ? 1 : 0]);
|
||||
await db.prepare("run", `INSERT INTO "thumbnailVotes" ("UUID", "votes", "locked", "shadowHidden") VALUES (?, 0, ?, ?)`,
|
||||
[UUID, isVip ? 1 : 0, isBanned ? 1 : 0]);
|
||||
|
||||
if (!thumbnail.original) {
|
||||
await db.prepare("run", `INSERT INTO "thumbnailTimestamps" ("UUID", "timestamp") VALUES (?, ?)`,
|
||||
@@ -190,4 +200,4 @@ async function verifyOldSubmissions(hashedUserID: HashedUserID, verification: nu
|
||||
await db.prepare("run", `UPDATE "titleVotes" as tv SET "verification" = ? FROM "titles" WHERE "titles"."UUID" = tv."UUID" AND "titles"."userID" = ? AND tv."verification" < ?`, [verification, hashedUserID, verification]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -172,7 +172,7 @@ async function checkUserActiveWarning(userID: HashedUserID): Promise<CheckResult
|
||||
) as {reason: string}[]).sort((a, b) => (b?.reason?.length ?? 0) - (a?.reason?.length ?? 0));
|
||||
|
||||
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"
|
||||
const defaultMessage = "Submission 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.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
|
||||
+ `Your userID is ${userID}.`;
|
||||
@@ -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;
|
||||
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. " +
|
||||
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)
|
||||
);
|
||||
|
||||
|
||||
26
src/utils/checkBan.ts
Normal file
26
src/utils/checkBan.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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, ipBanStatus] = await Promise.all([isUserBanned(userID), 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;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ describe("postBranding", () => {
|
||||
const userID6 = `PostBrandingUser6${".".repeat(16)}`;
|
||||
const userID7 = `PostBrandingUser7${".".repeat(16)}`;
|
||||
const userID8 = `PostBrandingUser8${".".repeat(16)}`;
|
||||
const bannedUser = `BannedPostBrandingUser${".".repeat(16)}`;
|
||||
|
||||
|
||||
const endpoint = "/api/branding";
|
||||
@@ -36,6 +37,9 @@ describe("postBranding", () => {
|
||||
await db.prepare("run", insertVipUserQuery, [getHash(vipUser)]);
|
||||
await db.prepare("run", insertVipUserQuery, [getHash(vipUser2)]);
|
||||
|
||||
const insertBannedUserQuery = 'INSERT INTO "shadowBannedUsers" ("userID") VALUES (?)';
|
||||
await db.prepare("run", insertBannedUserQuery, [getHash(bannedUser)]);
|
||||
|
||||
const insertTitleQuery = 'INSERT INTO "titles" ("videoID", "title", "original", "userID", "service", "hashedVideoID", "timeSubmitted", "UUID") VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
await db.prepare("run", insertTitleQuery, ["postBrandLocked1", "Some title", 0, getHash(userID1), Service.YouTube, getHash("postBrandLocked1"), Date.now(), "postBrandLocked1"]);
|
||||
await db.prepare("run", insertTitleQuery, ["postBrandLocked2", "Some title", 1, getHash(userID2), Service.YouTube, getHash("postBrandLocked2"), Date.now(), "postBrandLocked2"]);
|
||||
@@ -61,6 +65,18 @@ describe("postBranding", () => {
|
||||
await db.prepare("run", insertSegment, ["postBrandVerified3", 1, 11, 1, 0, "postBrandVerified3", getHash(userID8), 0, 50, "sponsor", "skip", "YouTube", 100, 0, 0, ""]);
|
||||
await db.prepare("run", insertSegment, ["postBrandVerified3", 11, 21, 1, 0, "postBrandVerified32", getHash(userID8), 0, 50, "sponsor", "skip", "YouTube", 100, 0, 0, ""]);
|
||||
await db.prepare("run", insertSegment, ["postBrandVerified3", 21, 31, 1, 0, "postBrandVerified33", getHash(userID8), 0, 50, "sponsor", "skip", "YouTube", 100, 0, 0, ""]);
|
||||
|
||||
// Testing details for banned user handling
|
||||
await db.prepare("run", insertTitleQuery, ["postBrandBannedCustomVote", "Some title", 0, getHash(userID1), Service.YouTube, getHash("postBrandBannedCustomVote"), Date.now(), "postBrandBannedCustomVote"]);
|
||||
await db.prepare("run", insertTitleQuery, ["postBrandBannedOriginalVote", "Some title", 1, getHash(userID1), Service.YouTube, getHash("postBrandBannedOriginalVote"), Date.now(), "postBrandBannedOriginalVote"]);
|
||||
await db.prepare("run", insertTitleVotesQuery, ["postBrandBannedCustomVote", 0, 0, 0, 0]);
|
||||
await db.prepare("run", insertTitleVotesQuery, ["postBrandBannedOriginalVote", 0, 0, 0, 0]);
|
||||
await db.prepare("run", insertThumbnailQuery, ["postBrandBannedCustomVote", 0, getHash(userID1), Service.YouTube, getHash("postBrandBannedCustomVote"), Date.now(), "postBrandBannedCustomVote"]);
|
||||
await db.prepare("run", insertThumbnailQuery, ["postBrandBannedOriginalVote", 1, getHash(userID1), Service.YouTube, getHash("postBrandBannedOriginalVote"), Date.now(), "postBrandBannedOriginalVote"]);
|
||||
await db.prepare("run", insertThumbnailVotesQuery, ["postBrandBannedCustomVote", 0, 0, 0]);
|
||||
await db.prepare("run", insertThumbnailVotesQuery, ["postBrandBannedOriginalVote", 0, 0, 0]);
|
||||
const insertThumbnailTimestampQuery = 'INSERT INTO "thumbnailTimestamps" ("UUID", "timestamp") VALUES (?, ?)';
|
||||
await db.prepare("run", insertThumbnailTimestampQuery, ["postBrandBannedCustomVote", 12.34]);
|
||||
});
|
||||
|
||||
it("Submit only title", async () => {
|
||||
@@ -579,4 +595,193 @@ describe("postBranding", () => {
|
||||
|
||||
assert.strictEqual(dbVotes.verification, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it("Banned users should not be able to vote (custom title)", async () => {
|
||||
const videoID = "postBrandBannedCustomVote";
|
||||
const title = {
|
||||
title: "Some title",
|
||||
original: false
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
title,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbTitle = await queryTitleByVideo(videoID);
|
||||
const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID);
|
||||
|
||||
assert.strictEqual(dbTitle.title, title.title);
|
||||
assert.strictEqual(dbTitle.original, title.original ? 1 : 0);
|
||||
|
||||
assert.strictEqual(dbVotes.votes, 0);
|
||||
assert.strictEqual(dbVotes.locked, 0);
|
||||
assert.strictEqual(dbVotes.shadowHidden, 0);
|
||||
});
|
||||
|
||||
it("Banned users should not be able to vote (original title)", async () => {
|
||||
const videoID = "postBrandBannedOriginalVote";
|
||||
const title = {
|
||||
title: "Some title",
|
||||
original: true
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
title,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbTitle = await queryTitleByVideo(videoID);
|
||||
const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID);
|
||||
|
||||
assert.strictEqual(dbTitle.title, title.title);
|
||||
assert.strictEqual(dbTitle.original, title.original ? 1 : 0);
|
||||
|
||||
assert.strictEqual(dbVotes.votes, 0);
|
||||
assert.strictEqual(dbVotes.locked, 0);
|
||||
assert.strictEqual(dbVotes.shadowHidden, 0);
|
||||
});
|
||||
|
||||
it("Banned users should not be able to vote (custom thumbnail)", async () => {
|
||||
const videoID = "postBrandBannedCustomVote";
|
||||
const thumbnail = {
|
||||
original: false,
|
||||
timestamp: 12.34
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
thumbnail,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbThumbnail = await queryThumbnailByVideo(videoID);
|
||||
const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID);
|
||||
|
||||
assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0);
|
||||
|
||||
assert.strictEqual(dbVotes.votes, 0);
|
||||
assert.strictEqual(dbVotes.locked, 0);
|
||||
assert.strictEqual(dbVotes.shadowHidden, 0);
|
||||
});
|
||||
|
||||
it("Banned users should not be able to vote (original thumbnail)", async () => {
|
||||
const videoID = "postBrandBannedOriginalVote";
|
||||
const thumbnail = {
|
||||
original: true
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
thumbnail,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbThumbnail = await queryThumbnailByVideo(videoID);
|
||||
const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID);
|
||||
|
||||
assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0);
|
||||
|
||||
assert.strictEqual(dbVotes.votes, 0);
|
||||
assert.strictEqual(dbVotes.locked, 0);
|
||||
assert.strictEqual(dbVotes.shadowHidden, 0);
|
||||
});
|
||||
|
||||
it("Banned users' custom submissions should be hidden (title)", async () => {
|
||||
const videoID = "postBrandBannedCustom";
|
||||
const title = {
|
||||
title: "Some title",
|
||||
original: false
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
title,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbTitle = await queryTitleByVideo(videoID);
|
||||
const dbVotes = await queryTitleVotesByUUID(dbTitle.UUID);
|
||||
|
||||
assert.strictEqual(dbTitle.title, title.title);
|
||||
assert.strictEqual(dbTitle.original, title.original ? 1 : 0);
|
||||
|
||||
assert.strictEqual(dbVotes.votes, 0);
|
||||
assert.strictEqual(dbVotes.locked, 0);
|
||||
assert.strictEqual(dbVotes.shadowHidden, 1);
|
||||
});
|
||||
|
||||
it("Banned users' custom submissions should be hidden (thumbnail)", async () => {
|
||||
const videoID = "postBrandBannedCustom";
|
||||
const thumbnail = {
|
||||
original: false,
|
||||
timestamp: 12.34
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
thumbnail,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbThumbnail = await queryThumbnailByVideo(videoID);
|
||||
const dbVotes = await queryThumbnailVotesByUUID(dbThumbnail.UUID);
|
||||
|
||||
assert.strictEqual(dbThumbnail.original, thumbnail.original ? 1 : 0);
|
||||
|
||||
assert.strictEqual(dbVotes.votes, 0);
|
||||
assert.strictEqual(dbVotes.locked, 0);
|
||||
assert.strictEqual(dbVotes.shadowHidden, 1);
|
||||
});
|
||||
|
||||
it("Banned users' original submissions should be ignored (title)", async () => {
|
||||
const videoID = "postBrandBannedOriginal";
|
||||
const title = {
|
||||
title: "Some title",
|
||||
original: true
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
title,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbTitle = await queryTitleByVideo(videoID);
|
||||
assert.strictEqual(dbTitle, undefined);
|
||||
});
|
||||
|
||||
it("Banned users' original submissions should be ignored (thumbnail)", async () => {
|
||||
const videoID = "postBrandBannedOriginal";
|
||||
const thumbnail = {
|
||||
original: true
|
||||
};
|
||||
|
||||
const res = await postBranding({
|
||||
thumbnail,
|
||||
userID: bannedUser,
|
||||
service: Service.YouTube,
|
||||
videoID
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
const dbThumbnail = await queryThumbnailByVideo(videoID);
|
||||
assert.strictEqual(dbThumbnail, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("postSkipSegments Warnings", () => {
|
||||
assert.strictEqual(res.status, 403);
|
||||
const errorMessage = res.data;
|
||||
const reason = "Reason01";
|
||||
const expected = "Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes"
|
||||
const expected = "Submission 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.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
|
||||
+ `Your userID is ${warnUser01Hash}.\n\nWarning reason: '${reason}'`;
|
||||
@@ -115,7 +115,7 @@ describe("postSkipSegments Warnings", () => {
|
||||
.then(res => {
|
||||
assert.strictEqual(res.status, 403);
|
||||
const errorMessage = res.data;
|
||||
const expected = "Submission rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes"
|
||||
const expected = "Submission 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.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app so we can further help you? "
|
||||
+ `Your userID is ${warnUser04Hash}.`;
|
||||
|
||||
Reference in New Issue
Block a user