Merge pull request #558 from mini-bomba/dearrow-bans

Fix Dearrow bans + some bug fixes
This commit is contained in:
Ajay Ramachandran
2023-09-04 02:43:39 -04:00
committed by GitHub
10 changed files with 323 additions and 73 deletions

View 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;

View File

@@ -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;
}

View File

@@ -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}

View File

@@ -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]);
}
}
}
}

View File

@@ -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
],
);

View File

@@ -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;
}
}

View File

@@ -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
View 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;
}

View File

@@ -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);
});
});

View File

@@ -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}.`;