Merge branch 'master' into fix/requiredSegments-hidden

# Conflicts:
#	src/routes/getSkipSegments.ts
This commit is contained in:
mini-bomba
2022-07-21 00:54:14 +02:00
13 changed files with 122 additions and 81 deletions

View File

@@ -18,7 +18,7 @@ export async function getSavedTimeForUser(req: Request, res: Response): Promise<
userID = await getHashCache(userID);
try {
const row = await db.prepare("get", 'SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -1 AND "shadowHidden" != 1 ', [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, userID]);
const row = await db.prepare("get", 'SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -1 AND "shadowHidden" != 1 ', [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, userID], { useReplica: true });
if (row.minutesSaved != null) {
return res.send({

View File

@@ -37,7 +37,7 @@ async function prepareCategorySegments(req: Request, videoID: VideoID, service:
const service = getService(req?.query?.service as string);
const fetchData = () => privateDB.prepare("all", 'SELECT "hashedIP" FROM "sponsorTimes" WHERE "videoID" = ? AND "timeSubmitted" = ? AND "service" = ?',
[videoID, segment.timeSubmitted, service]) as Promise<{ hashedIP: HashedIP }[]>;
[videoID, segment.timeSubmitted, service], { useReplica: true }) as Promise<{ hashedIP: HashedIP }[]>;
cache.shadowHiddenSegmentIPs[videoID][segment.timeSubmitted] = await QueryCacher.get(fetchData, shadowHiddenIPKey(videoID, segment.timeSubmitted, service));
}
@@ -175,7 +175,8 @@ async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service
"all",
`SELECT "videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "hashedVideoID", "timeSubmitted", "description" FROM "sponsorTimes"
WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "startTime"`,
[`${hashedVideoIDPrefix}%`, service]
[`${hashedVideoIDPrefix}%`, service],
{ useReplica: true }
) as Promise<DBSegment[]>;
if (hashedVideoIDPrefix.length === 4) {
@@ -191,7 +192,8 @@ async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): P
"all",
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "videoDuration", "hidden", "reputation", "shadowHidden", "timeSubmitted", "description" FROM "sponsorTimes"
WHERE "videoID" = ? AND "service" = ? ORDER BY "startTime"`,
[videoID, service]
[videoID, service],
{ useReplica: true }
) as Promise<DBSegment[]>;
return await QueryCacher.get(fetchFromDB, skipSegmentsKey(videoID, service));
@@ -283,7 +285,7 @@ async function chooseSegments(videoID: VideoID, service: Service, segments: DBSe
//Segments with less than -1 votes are already ignored before this function is called
async function buildSegmentGroups(segments: DBSegment[]): Promise<OverlappingSegmentGroup[]> {
const reputationPromises = segments.map(segment =>
segment.userID ? getReputation(segment.userID) : null);
segment.userID ? getReputation(segment.userID).catch((e) => Logger.error(e)) : null);
//Create groups of segments that are similar to eachother
//Segments must be sorted by their startTime so that we can build groups chronologically:
@@ -306,7 +308,7 @@ async function buildSegmentGroups(segments: DBSegment[]): Promise<OverlappingSeg
currentGroup.votes += segment.votes;
}
if (segment.userID) segment.reputation = Math.min(segment.reputation, await reputationPromises[i]);
if (segment.userID) segment.reputation = Math.min(segment.reputation, (await reputationPromises[i]) || Infinity);
if (segment.reputation > 0) {
currentGroup.reputation += segment.reputation;
}

View File

@@ -15,7 +15,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
const row = await db.prepare("get",
`SELECT SUM(((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views") as "minutesSaved",
count(*) as "segmentCount" FROM "sponsorTimes"
WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [maxRewardTime, maxRewardTime, userID]);
WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [maxRewardTime, maxRewardTime, userID], { useReplica: true });
if (row.minutesSaved != null) {
return {
minutesSaved: row.minutesSaved,
@@ -34,7 +34,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
async function dbGetIgnoredSegmentCount(userID: HashedUserID): Promise<number> {
try {
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID]);
const row = await db.prepare("get", `SELECT COUNT(*) as "ignoredSegmentCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
return row?.ignoredSegmentCount ?? 0;
} catch (err) {
return null;
@@ -52,7 +52,7 @@ async function dbGetUsername(userID: HashedUserID) {
async function dbGetViewsForUser(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID]);
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ? AND "votes" > -2 AND "shadowHidden" != 1`, [userID], { useReplica: true });
return row?.viewCount ?? 0;
} catch (err) {
return false;
@@ -61,7 +61,7 @@ async function dbGetViewsForUser(userID: HashedUserID) {
async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID]);
const row = await db.prepare("get", `SELECT SUM("views") as "ignoredViewCount" FROM "sponsorTimes" WHERE "userID" = ? AND ( "votes" <= -2 OR "shadowHidden" = 1 )`, [userID], { useReplica: true });
return row?.ignoredViewCount ?? 0;
} catch (err) {
return false;
@@ -70,7 +70,7 @@ async function dbGetIgnoredViewsForUser(userID: HashedUserID) {
async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
try {
const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID]);
const row = await db.prepare("get", `SELECT COUNT(*) as total FROM "warnings" WHERE "userID" = ? AND "enabled" = 1`, [userID], { useReplica: true });
return row?.total ?? 0;
} catch (err) {
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
@@ -80,7 +80,7 @@ async function dbGetWarningsForUser(userID: HashedUserID): Promise<number> {
async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUID> {
try {
const row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID]);
const row = await db.prepare("get", `SELECT "UUID" FROM "sponsorTimes" WHERE "userID" = ? ORDER BY "timeSubmitted" DESC LIMIT 1`, [userID], { useReplica: true });
return row?.UUID ?? null;
} catch (err) {
return null;
@@ -89,7 +89,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUI
async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<string> {
try {
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID]);
const row = await db.prepare("get", `SELECT reason FROM "warnings" WHERE "userID" = ? AND "enabled" = 1 ORDER BY "issueTime" DESC LIMIT 1`, [userID], { useReplica: true });
return row?.reason ?? "";
} catch (err) {
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
@@ -99,7 +99,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<st
async function dbGetBanned(userID: HashedUserID): Promise<boolean> {
try {
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]);
const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true });
return row?.userCount > 0 ?? false;
} catch (err) {
return false;

View File

@@ -15,7 +15,7 @@ export async function getUsername(req: Request, res: Response): Promise<Response
userID = await getHashCache(userID);
try {
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID], { useReplica: true });
if (row !== undefined) {
return res.send({

View File

@@ -15,7 +15,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise<Resp
userID = await getHashCache(userID);
try {
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]);
const row = await db.prepare("get", `SELECT SUM("views") as "viewCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID], { useReplica: true });
//increase the view count by one
if (row.viewCount != null) {

View File

@@ -52,8 +52,8 @@ export async function postWarning(req: Request, res: Response): Promise<Response
// check if warning is still within issue time and warning is not enabled
} else if (checkExpiredWarning(previousWarning) ) {
await db.prepare(
"run", 'UPDATE "warnings" SET "enabled" = 1 WHERE "userID" = ? AND "issueTime" = ?',
[userID, previousWarning.issueTime]
"run", 'UPDATE "warnings" SET "enabled" = 1, "reason" = ? WHERE "userID" = ? AND "issueTime" = ?',
[reason, userID, previousWarning.issueTime]
);
resultStatus = "re-enabled";
} else {

View File

@@ -211,7 +211,7 @@ async function sendWebhooks(voteData: VoteData) {
async function categoryVote(UUID: SegmentUUID, userID: UserID, 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]);
const usersLastVoteInfo = await privateDB.prepare("get", `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID], { useReplica: true });
if (usersLastVoteInfo?.category === category) {
// Double vote, ignore
@@ -219,7 +219,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
}
const segmentInfo = (await db.prepare("get", `SELECT "category", "actionType", "videoID", "hashedVideoID", "service", "userID", "locked" FROM "sponsorTimes" WHERE "UUID" = ?`,
[UUID])) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
[UUID], { useReplica: true })) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
if (segmentInfo.actionType === ActionType.Full) {
return { status: 400, message: "Not allowed to change category of a full video segment" };
@@ -232,7 +232,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
}
// Ignore vote if the next category is locked
const nextCategoryLocked = await db.prepare("get", `SELECT "videoID", "category" FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?`, [segmentInfo.videoID, segmentInfo.service, category]);
const nextCategoryLocked = await db.prepare("get", `SELECT "videoID", "category" FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?`, [segmentInfo.videoID, segmentInfo.service, category], { useReplica: true });
if (nextCategoryLocked && !isVIP) {
return { status: 200 };
}
@@ -242,12 +242,13 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
return { status: 200 };
}
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category]);
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category], { useReplica: true });
const timeSubmitted = Date.now();
const voteAmount = (isVIP || isTempVIP) ? 500 : 1;
const ableToVote = isVIP || isTempVIP || finalResponse.finalStatus === 200 || true;
const ableToVote = finalResponse.finalStatus === 200
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID], { useReplica: true })) === undefined;
if (ableToVote) {
// Add the vote
@@ -270,15 +271,15 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
}
// See if the submissions category is ready to change
const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, segmentInfo.category]);
const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, segmentInfo.category], { useReplica: true });
const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID], { useReplica: true });
const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
const startingVotes = isSubmissionVIP ? 10000 : 1;
// Change this value from 1 in the future to make it harder to change categories
// Done this way without ORs incase the value is zero
const currentCategoryCount = (currentCategoryInfo === undefined || currentCategoryInfo === null) ? startingVotes : currentCategoryInfo.votes;
const currentCategoryCount = currentCategoryInfo?.votes ?? startingVotes;
// Add submission as vote
if (!currentCategoryInfo && submissionInfo) {
@@ -290,7 +291,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
//TODO: In the future, raise this number from zero to make it harder to change categories
// VIPs change it every time
if (nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2) || isVIP || isTempVIP || isOwnSubmission) {
if (isVIP || isTempVIP || isOwnSubmission || nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2)) {
// Replace the category
await db.prepare("run", `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
}
@@ -367,6 +368,19 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
return { status: 400 };
}
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const warnings = (await db.prepare("all", `SELECT "reason" FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
));
if (warnings.length >= config.maxNumberOfActiveWarnings) {
const warningReason = warnings[0]?.reason;
return { status: 403, message: "Vote rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. " +
"Could you please send a message in Discord or Matrix so we can further help you?" +
`${(warningReason.length > 0 ? ` Warning reason: '${warningReason}'` : "")}` };
}
// no type but has category, categoryVote
if (!type && category) {
return categoryVote(UUID, nonAnonUserID, isVIP, isTempVIP, isOwnSubmission, category, hashedIP, finalResponse);
@@ -377,7 +391,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
const isSegmentLocked = segmentInfo.locked;
const isVideoLocked = async () => !!(await db.prepare("get", `SELECT "category" FROM "lockCategories" WHERE
"videoID" = ? AND "service" = ? AND "category" = ? AND "actionType" = ?`,
[segmentInfo.videoID, segmentInfo.service, segmentInfo.category, segmentInfo.actionType]));
[segmentInfo.videoID, segmentInfo.service, segmentInfo.category, segmentInfo.actionType], { useReplica: true }));
if (isSegmentLocked || await isVideoLocked()) {
finalResponse.blockVote = true;
finalResponse.webhookType = VoteWebhookType.Rejected;
@@ -396,19 +410,6 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
}
}
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const warnings = (await db.prepare("all", `SELECT "reason" FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
));
if (warnings.length >= config.maxNumberOfActiveWarnings) {
const warningReason = warnings[0]?.reason;
return { status: 403, message: "Vote rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. " +
"Could you please send a message in Discord or Matrix so we can further help you?" +
`${(warningReason.length > 0 ? ` Warning reason: '${warningReason}'` : "")}` };
}
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
// no restrictions on checkDuration
@@ -419,7 +420,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
try {
// check if vote has already happened
const votesRow = await privateDB.prepare("get", `SELECT "type" FROM "votes" WHERE "userID" = ? AND "UUID" = ?`, [userID, UUID]);
const votesRow = await privateDB.prepare("get", `SELECT "type" FROM "votes" WHERE "userID" = ? AND "UUID" = ?`, [userID, UUID], { useReplica: true });
// -1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
// oldIncrementAmount will be zero if row is null
@@ -477,9 +478,9 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID
&& !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter)
&& !finalResponse.blockVote
&& finalResponse.finalStatus === 200
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID])) === undefined
&& (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID])) === undefined);
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) !== undefined
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) === undefined
&& (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID], { useReplica: true })) === undefined);
const ableToVote = isVIP || isTempVIP || userAbleToVote;