Add casual mode endpoint

This commit is contained in:
Ajay
2025-02-05 03:38:55 -05:00
parent ab9cab8ff5
commit 07435b9af1
13 changed files with 409 additions and 16 deletions

View File

@@ -3,7 +3,7 @@ import { isEmpty } from "lodash";
import { config } from "../config";
import { db, privateDB } from "../databases/databases";
import { Postgres } from "../databases/Postgres";
import { BrandingDBSubmission, BrandingDBSubmissionData, BrandingHashDBResult, BrandingResult, BrandingSegmentDBResult, BrandingSegmentHashDBResult, ThumbnailDBResult, ThumbnailResult, TitleDBResult, TitleResult } from "../types/branding.model";
import { BrandingDBSubmission, BrandingDBSubmissionData, BrandingHashDBResult, BrandingResult, BrandingSegmentDBResult, BrandingSegmentHashDBResult, CasualVoteDBResult, CasualVoteHashDBResult, ThumbnailDBResult, ThumbnailResult, TitleDBResult, TitleResult } from "../types/branding.model";
import { HashedIP, IPAddress, Service, VideoID, VideoIDHash, Visibility } from "../types/segments.model";
import { shuffleArray } from "../utils/array";
import { getHashCache } from "../utils/getHashCache";
@@ -51,10 +51,20 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
{ useReplica: true }
) as Promise<BrandingSegmentDBResult[]>;
const getCasualVotes = () => db.prepare(
"all",
`SELECT "category", "upvotes", "downvotes" FROM "casualVotes"
WHERE "videoID" = ? AND "service" = ?
ORDER BY "timeSubmitted" ASC`,
[videoID, service],
{ useReplica: true }
) as Promise<CasualVoteDBResult[]>;
const getBranding = async () => {
const titles = getTitles();
const thumbnails = getThumbnails();
const segments = getSegments();
const casualVotes = getCasualVotes();
for (const title of await titles) {
title.title = title.title.replace("<", "");
@@ -63,7 +73,8 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
return {
titles: await titles,
thumbnails: await thumbnails,
segments: await segments
segments: await segments,
casualVotes: await casualVotes
};
};
@@ -85,7 +96,8 @@ export async function getVideoBranding(res: Response, videoID: VideoID, service:
currentIP: null as Promise<HashedIP> | null
};
return filterAndSortBranding(videoID, returnUserID, fetchAll, branding.titles, branding.thumbnails, branding.segments, ip, cache);
return filterAndSortBranding(videoID, returnUserID, fetchAll, branding.titles,
branding.thumbnails, branding.segments, branding.casualVotes, ip, cache);
}
export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, service: Service, ip: IPAddress, returnUserID: boolean, fetchAll: boolean): Promise<Record<VideoID, BrandingResult>> {
@@ -117,12 +129,22 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
{ useReplica: true }
) as Promise<BrandingSegmentHashDBResult[]>;
const getCasualVotes = () => db.prepare(
"all",
`SELECT "videoID", "category", "upvotes", "downvotes" FROM "casualVotes"
WHERE "hashedVideoID" LIKE ? AND "service" = ?
ORDER BY "timeSubmitted" ASC`,
[`${videoHashPrefix}%`, service],
{ useReplica: true }
) as Promise<CasualVoteHashDBResult[]>;
const branding = await QueryCacher.get(async () => {
// Make sure they are both called in parallel
const branding = {
titles: getTitles(),
thumbnails: getThumbnails(),
segments: getSegments()
segments: getSegments(),
casualVotes: getCasualVotes()
};
const dbResult: Record<VideoID, BrandingHashDBResult> = {};
@@ -130,7 +152,8 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
dbResult[submission.videoID] = dbResult[submission.videoID] || {
titles: [],
thumbnails: [],
segments: []
segments: [],
casualVotes: []
};
};
@@ -150,6 +173,11 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
dbResult[segment.videoID].segments.push(segment);
});
(await branding.casualVotes).forEach((casualVote) => {
initResult(casualVote);
dbResult[casualVote.videoID].casualVotes.push(casualVote);
});
return dbResult;
}, brandingHashKey(videoHashPrefix, service));
@@ -162,14 +190,14 @@ export async function getVideoBrandingByHash(videoHashPrefix: VideoIDHash, servi
await Promise.all(Object.keys(branding).map(async (key) => {
const castedKey = key as VideoID;
processedResult[castedKey] = await filterAndSortBranding(castedKey, returnUserID, fetchAll, branding[castedKey].titles,
branding[castedKey].thumbnails, branding[castedKey].segments, ip, cache);
branding[castedKey].thumbnails, branding[castedKey].segments, branding[castedKey].casualVotes, ip, cache);
}));
return processedResult;
}
async function filterAndSortBranding(videoID: VideoID, returnUserID: boolean, fetchAll: boolean, dbTitles: TitleDBResult[],
dbThumbnails: ThumbnailDBResult[], dbSegments: BrandingSegmentDBResult[],
dbThumbnails: ThumbnailDBResult[], dbSegments: BrandingSegmentDBResult[], dbCasualVotes: CasualVoteDBResult[],
ip: IPAddress, cache: { currentIP: Promise<HashedIP> | null }): Promise<BrandingResult> {
const shouldKeepTitles = shouldKeepSubmission(dbTitles, BrandingSubmissionType.Title, ip, cache);
@@ -202,11 +230,17 @@ async function filterAndSortBranding(videoID: VideoID, returnUserID: boolean, fe
}))
.filter((a) => (fetchAll && !a.original) || a.votes >= 1 || (a.votes >= 0 && !a.original) || a.locked) as ThumbnailResult[];
const casualVotes = dbCasualVotes.map((r) => ({
id: r.category,
count: r.upvotes - r.downvotes
})).filter((a) => a.count > 0);
const videoDuration = dbSegments.filter(s => s.videoDuration !== 0)[0]?.videoDuration ?? null;
return {
titles,
thumbnails,
casualVotes,
randomTime: findRandomTime(videoID, dbSegments, videoDuration),
videoDuration: videoDuration,
};
@@ -303,7 +337,7 @@ export async function getBranding(req: Request, res: Response) {
.then(etag => res.set("ETag", etag))
.catch(() => null);
const status = result.titles.length > 0 || result.thumbnails.length > 0 ? 200 : 404;
const status = result.titles.length > 0 || result.thumbnails.length > 0 || result.casualVotes.length > 0 ? 200 : 404;
return res.status(status).json(result);
} catch (e) {
Logger.error(e as string);

111
src/routes/postCasual.ts Normal file
View File

@@ -0,0 +1,111 @@
import { Request, Response } from "express";
import { config } from "../config";
import { db, privateDB } from "../databases/databases";
import { BrandingUUID, CasualCategory, CasualVoteSubmission } from "../types/branding.model";
import { HashedIP, IPAddress, Service, VideoID } from "../types/segments.model";
import { HashedUserID } from "../types/user.model";
import { getHashCache } from "../utils/getHashCache";
import { getIP } from "../utils/getIP";
import { getService } from "../utils/getService";
import { Logger } from "../utils/logger";
import crypto from "crypto";
import { QueryCacher } from "../utils/queryCacher";
import { acquireLock } from "../utils/redisLock";
import { checkBanStatus } from "../utils/checkBan";
enum CasualVoteType {
Upvote = 1,
Downvote = 2
}
interface ExistingVote {
UUID: BrandingUUID;
type: number;
}
export async function postCasual(req: Request, res: Response) {
const { videoID, userID, downvote, category } = req.body as CasualVoteSubmission;
const service = getService(req.body.service);
if (!videoID || !userID || userID.length < 30 || !service || !category) {
return res.status(400).send("Bad Request");
}
if (!config.casualCategoryList.includes(category)) {
return res.status(400).send("Invalid category");
}
try {
const hashedUserID = await getHashCache(userID);
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(`postCasual:${videoID}.${hashedUserID}`);
if (!lock.status) {
res.status(429).send("Vote already in progress");
return;
}
if (isBanned) {
return res.status(200).send("OK");
}
const now = Date.now();
const voteType: CasualVoteType = downvote ? CasualVoteType.Downvote : CasualVoteType.Upvote;
const existingUUID = (await db.prepare("get", `SELECT "UUID" from "casualVotes" where "videoID" = ? AND "category" = ?`, [videoID, category]))?.UUID;
const UUID = existingUUID || crypto.randomUUID();
const alreadyVotedTheSame = await handleExistingVotes(videoID, service, UUID, hashedUserID, hashedIP, category, voteType, now);
if (existingUUID) {
if (!alreadyVotedTheSame) {
if (downvote) {
await db.prepare("run", `UPDATE "casualVotes" SET "downvotes" = "downvotes" + 1 WHERE "UUID" = ?`, [UUID]);
} else {
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" + 1 WHERE "UUID" = ?`, [UUID]);
}
}
} else {
if (downvote) {
throw new Error("Title submission doesn't exist");
}
await db.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "hashedVideoID", "timeSubmitted", "UUID", "category", "upvotes", "downvotes") VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[videoID, service, hashedVideoID, now, UUID, category, downvote ? 0 : 1, downvote ? 1 : 0]);
}
//todo: cache clearing
QueryCacher.clearBrandingCache({ videoID, hashedVideoID, service });
res.status(200).send("OK");
lock.unlock();
} catch (e) {
Logger.error(e as string);
res.status(500).send("Internal Server Error");
}
}
async function handleExistingVotes(videoID: VideoID, service: Service, UUID: string,
hashedUserID: HashedUserID, hashedIP: HashedIP, category: CasualCategory, voteType: CasualVoteType, now: number): Promise<boolean> {
const existingVote = await privateDB.prepare("get", `SELECT "UUID", "type" from "casualVotes" WHERE "videoID" = ? AND "service" = ? AND "userID" = ? AND category = ?`, [videoID, service, hashedUserID, category]) as ExistingVote;
if (existingVote) {
if (existingVote.type === voteType) {
return true;
}
if (existingVote.type === CasualVoteType.Upvote) {
await db.prepare("run", `UPDATE "casualVotes" SET "upvotes" = "upvotes" - 1 WHERE "UUID" = ?`, [UUID]);
} else {
await db.prepare("run", `UPDATE "casualVotes" SET "downvotes" = "downvotes" - 1 WHERE "UUID" = ?`, [UUID]);
}
await privateDB.prepare("run", `DELETE FROM "casualVotes" WHERE "UUID" = ?`, [existingVote.UUID]);
}
await privateDB.prepare("run", `INSERT INTO "casualVotes" ("videoID", "service", "userID", "hashedIP", "category", "type", "timeSubmitted") VALUES (?, ?, ?, ?, ?, ?, ?)`,
[videoID, service, hashedUserID, hashedIP, category, voteType, now]);
return false;
}