This commit is contained in:
Ajay
2022-10-29 21:04:01 -04:00
78 changed files with 2473 additions and 260 deletions

View File

@@ -45,6 +45,8 @@ import { youtubeApiProxy } from "./routes/youtubeApiProxy";
import { getChapterNames } from "./routes/getChapterNames";
import { getTopCategoryUsers } from "./routes/getTopCategoryUsers";
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
import { endpoint as getVideoLabels } from "./routes/getVideoLabel";
import { getVideoLabelsByHash } from "./routes/getVideoLabelByHash";
import { addFeature } from "./routes/addFeature";
import { generateTokenRequest } from "./routes/generateToken";
import { verifyTokenRequest } from "./routes/verifyToken";
@@ -200,6 +202,11 @@ function setupRoutes(router: Router) {
router.get("/api/generateToken/:type", generateTokenRequest);
router.get("/api/verifyToken", verifyTokenRequest);
// labels
router.get("/api/videoLabels", getVideoLabels);
router.get("/api/videoLabels/:prefix", getVideoLabelsByHash);
/* istanbul ignore next */
if (config.postgres?.enabled) {
router.get("/database", (req, res) => dumpDatabase(req, res, true));
router.get("/database.json", (req, res) => dumpDatabase(req, res, false));
@@ -211,4 +218,4 @@ function setupRoutes(router: Router) {
});
}
}
/* eslint-enable @typescript-eslint/no-misused-promises */
/* eslint-enable @typescript-eslint/no-misused-promises */

View File

@@ -35,6 +35,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
|| actionTypes && !Array.isArray(actionTypes)
|| actionTypes.length === 0
) {
return res.status(400).json({
@@ -48,7 +49,7 @@ export async function deleteLockCategoriesEndpoint(req: DeleteLockCategoriesRequ
if (!userIsVIP) {
return res.status(403).json({
message: "Must be a VIP to mark videos.",
message: "Must be a VIP to lock videos.",
});
}

View File

@@ -24,6 +24,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo
if (type === TokenType.patreon || (type === TokenType.local && adminUserIDHash === config.adminUserID)) {
const licenseKey = await createAndSaveToken(type, code);
/* istanbul ignore else */
if (licenseKey) {
return res.status(200).send(`
<h1>
@@ -45,5 +46,7 @@ export async function generateTokenRequest(req: GenerateTokenRequest, res: Respo
</h1>
`);
}
} else {
return res.sendStatus(403);
}
}

View File

@@ -7,7 +7,11 @@ export async function getDaysSavedFormatted(req: Request, res: Response): Promis
if (row !== undefined) {
//send this result
return res.send({
daysSaved: row.daysSaved.toFixed(2),
daysSaved: row.daysSaved?.toFixed(2) ?? "0",
});
} else {
return res.send({
daysSaved: 0
});
}
}

View File

@@ -21,7 +21,7 @@ export async function getIsUserVIP(req: Request, res: Response): Promise<Respons
hashedUserID: hashedUserID,
vip: vipState,
});
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -33,7 +33,7 @@ export async function getLockCategories(req: Request, res: Response): Promise<Re
categories,
actionTypes
});
} catch (err) {
} catch (err) /* istanbul ignore next */{
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -44,14 +44,25 @@ const mergeLocks = (source: DBLock[], actionTypes: ActionType[]): LockResultByHa
export async function getLockCategoriesByHash(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
const actionTypes: ActionType[] = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
let actionTypes: ActionType[] = [];
try {
actionTypes = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
if (!Array.isArray(actionTypes)) {
//invalid request
return res.sendStatus(400);
}
} catch (err) {
//invalid request
return res.status(400).send("Invalid request: JSON parse error (actionTypes)");
}
if (!hashPrefixTester(req.params.prefix)) {
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
}
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
@@ -62,7 +73,7 @@ export async function getLockCategoriesByHash(req: Request, res: Response): Prom
if (lockedRows.length === 0 || !lockedRows[0]) return res.sendStatus(404);
// merge all locks
return res.send(mergeLocks(lockedRows, actionTypes));
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -32,18 +32,24 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
return res.status(400).send("No videoID provided");
}
let categories: Category[] = [];
const actionTypes: ActionType[] = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
const possibleCategories = filterActionType(actionTypes);
if (!Array.isArray(actionTypes)) {
//invalid request
return res.status(400).send("actionTypes parameter does not match format requirements");
let actionTypes: ActionType[] = [];
try {
actionTypes = req.query.actionTypes
? JSON.parse(req.query.actionTypes as string)
: req.query.actionType
? Array.isArray(req.query.actionType)
? req.query.actionType
: [req.query.actionType]
: [ActionType.Skip, ActionType.Mute];
if (!Array.isArray(actionTypes)) {
//invalid request
return res.status(400).send("actionTypes parameter does not match format requirements");
}
} catch (error) {
return res.status(400).send("Bad parameter: actionTypes (invalid JSON)");
}
const possibleCategories = filterActionType(actionTypes);
try {
categories = req.query.categories
? JSON.parse(req.query.categories as string)
@@ -64,11 +70,6 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
: categories.filter(x =>
possibleCategories.includes(x));
if (!videoID || !Array.isArray(actionTypes)) {
//invalid request
return res.sendStatus(400);
}
try {
// Get existing lock categories markers
const row = await db.prepare("all", 'SELECT "category", "reason", "actionType", "userID" from "lockCategories" where "videoID" = ?', [videoID]) as {category: Category, reason: string, actionType: ActionType, userID: string }[];
@@ -115,7 +116,7 @@ export async function getLockReason(req: Request, res: Response): Promise<Respon
}
return res.send(results);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -27,7 +27,7 @@ export async function getSavedTimeForUser(req: Request, res: Response): Promise<
} else {
return res.sendStatus(404);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`getSavedTimeForUser ${err}`);
return res.sendStatus(500);
}

View File

@@ -128,12 +128,7 @@ async function handleGetSegments(req: Request, res: Response): Promise<searchSeg
const segments = await getSegmentsFromDBByVideoID(videoID, service);
if (segments === null || segments === undefined) {
res.sendStatus(500);
return false;
}
if (segments.length === 0) {
if (!segments?.length) {
res.sendStatus(404);
return false;
}
@@ -155,6 +150,7 @@ function filterSegments(segments: DBSegment[], filters: Record<string, any>, pag
);
if (sortBy !== SortableFields.timeSubmitted) {
/* istanbul ignore next */
filteredSegments.sort((a,b) => {
const key = sortDir === "desc" ? 1 : -1;
if (a[sortBy] < b[sortBy]) {
@@ -187,6 +183,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
return res.send(segmentResponse);
}
} catch (err) {
/* istanbul ignore next */
if (err instanceof SyntaxError) {
return res.status(400).send("Invalid array in parameters");
} else return res.sendStatus(500);

View File

@@ -7,7 +7,7 @@ const isValidSegmentUUID = (str: string): boolean => /^([a-f0-9]{64}|[a-f0-9]{8}
async function getSegmentFromDBByUUID(UUID: SegmentUUID): Promise<DBSegment> {
try {
return await db.prepare("get", `SELECT * FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@@ -62,7 +62,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
//send result
return res.send(DBSegments);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err instanceof SyntaxError) { // catch JSON.parse error
return res.status(400).send("UUIDs parameter does not match format requirements.");
} else return res.sendStatus(500);

View File

@@ -107,7 +107,7 @@ async function getSegmentsByVideoID(req: Request, videoID: VideoID, categories:
}
return processedSegments;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err) {
Logger.error(err as string);
return null;
@@ -169,7 +169,7 @@ async function getSegmentsByHash(req: Request, hashedVideoIDPrefix: VideoIDHash,
}));
return segments;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return null;
}
@@ -465,7 +465,7 @@ async function endpoint(req: Request, res: Response): Promise<Response> {
//send result
return res.send(segments);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err instanceof SyntaxError) {
return res.status(400).send("Categories parameter does not match format requirements.");
} else return res.sendStatus(500);

View File

@@ -67,8 +67,6 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
// Get all video id's that match hash prefix
const segments = await getSegmentsByHash(req, hashPrefix, categories, actionTypes, requiredSegments, service);
if (!segments) return res.status(404).json([]);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
hash: data.hash,

View File

@@ -18,7 +18,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
processTime = Date.now() - dbStartTime;
return e.value;
})
.catch(e => {
.catch(e => /* istanbul ignore next */ {
Logger.error(`status: SQL query timed out: ${e}`);
return -1;
});
@@ -28,7 +28,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
.then(e => {
redisProcessTime = Date.now() - redisStartTime;
return e;
}).catch(e => {
}).catch(e => /* istanbul ignore next */ {
Logger.error(`status: redis increment timed out ${e}`);
return [-1];
});
@@ -36,7 +36,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
const statusValues: Record<string, any> = {
uptime: process.uptime(),
commit: (global as any).HEADCOMMIT || "unknown",
commit: (global as any)?.HEADCOMMIT ?? "unknown",
db: Number(dbVersion),
startTime,
processTime,
@@ -48,7 +48,7 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
activeRedisRequests: getRedisActiveRequests(),
};
return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -75,11 +75,6 @@ export async function getTopUsers(req: Request, res: Response): Promise<Response
const sortType = parseInt(req.query.sortType as string);
const categoryStatsEnabled = req.query.categoryStats;
if (sortType == undefined) {
//invalid request
return res.sendStatus(400);
}
//setup which sort type to use
let sortBy = "";
if (sortType == 0) {

View File

@@ -12,7 +12,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
try {
return db.prepare("all", `SELECT "userName", "userID" FROM "userNames" WHERE "userName"
LIKE ? ESCAPE '\\' LIMIT 10`, [userName]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@@ -20,7 +20,7 @@ function getFuzzyUserID(userName: string): Promise<{userName: string, userID: Us
function getExactUserID(userName: string): Promise<{userName: string, userID: UserID }[]> {
try {
return db.prepare("all", `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]);
} catch (err) {
} catch (err) /* istanbul ignore next */{
return null;
}
}
@@ -42,6 +42,7 @@ export async function getUserID(req: Request, res: Response): Promise<Response>
: await getFuzzyUserID(userName);
if (results === undefined || results === null) {
/* istanbul ignore next */
return res.sendStatus(500);
} else if (results.length === 0) {
return res.sendStatus(404);

View File

@@ -28,7 +28,7 @@ async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ min
segmentCount: 0,
};
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@@ -37,7 +37,7 @@ 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], { useReplica: true });
return row?.ignoredSegmentCount ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@@ -46,7 +46,7 @@ async function dbGetUsername(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
return row?.userName ?? userID;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@@ -55,7 +55,7 @@ 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], { useReplica: true });
return row?.viewCount ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@@ -64,7 +64,7 @@ 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], { useReplica: true });
return row?.ignoredViewCount ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@@ -73,7 +73,7 @@ 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], { useReplica: true });
return row?.total ?? 0;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Couldn't get warnings for user ${userID}. returning 0`);
return 0;
}
@@ -83,7 +83,7 @@ async function dbGetLastSegmentForUser(userID: HashedUserID): Promise<SegmentUUI
try {
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) {
} catch (err) /* istanbul ignore next */ {
return null;
}
}
@@ -92,7 +92,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise<st
try {
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) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Couldn't get reason for user ${userID}. returning blank`);
return "";
}
@@ -102,7 +102,7 @@ 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;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}
@@ -194,7 +194,7 @@ async function getUserInfo(req: Request, res: Response): Promise<Response> {
export async function endpoint(req: Request, res: Response): Promise<Response> {
try {
return await getUserInfo(req, res);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
if (err instanceof SyntaxError) { // catch JSON.parse error
return res.status(400).send("Invalid values JSON");
} else return res.sendStatus(500);

View File

@@ -75,7 +75,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea
};
}
return result;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return null;
}
@@ -85,7 +85,7 @@ async function dbGetUsername(userID: HashedUserID) {
try {
const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]);
return row?.userName ?? userID;
} catch (err) {
} catch (err) /* istanbul ignore next */ {
return false;
}
}

View File

@@ -27,7 +27,7 @@ export async function getUsername(req: Request, res: Response): Promise<Response
userName: userID,
});
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

167
src/routes/getVideoLabel.ts Normal file
View File

@@ -0,0 +1,167 @@
import { Request, Response } from "express";
import { db } from "../databases/databases";
import { videoLabelsHashKey, videoLabelsKey } from "../utils/redisKeys";
import { SBRecord } from "../types/lib.model";
import { DBSegment, Segment, Service, VideoData, VideoID, VideoIDHash } from "../types/segments.model";
import { Logger } from "../utils/logger";
import { QueryCacher } from "../utils/queryCacher";
import { getService } from "../utils/getService";
function transformDBSegments(segments: DBSegment[]): Segment[] {
return segments.map((chosenSegment) => ({
category: chosenSegment.category,
actionType: chosenSegment.actionType,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
locked: chosenSegment.locked,
votes: chosenSegment.votes,
videoDuration: chosenSegment.videoDuration,
userID: chosenSegment.userID,
description: chosenSegment.description
}));
}
async function getLabelsByVideoID(videoID: VideoID, service: Service): Promise<Segment[]> {
try {
const segments: DBSegment[] = await getSegmentsFromDBByVideoID(videoID, service);
return chooseSegment(segments);
} catch (err) {
if (err) {
Logger.error(err as string);
return null;
}
}
}
async function getLabelsByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<SBRecord<VideoID, VideoData>> {
const segments: SBRecord<VideoID, VideoData> = {};
try {
type SegmentWithHashPerVideoID = SBRecord<VideoID, { hash: VideoIDHash, segments: DBSegment[] }>;
const segmentPerVideoID: SegmentWithHashPerVideoID = (await getSegmentsFromDBByHash(hashedVideoIDPrefix, service))
.reduce((acc: SegmentWithHashPerVideoID, segment: DBSegment) => {
acc[segment.videoID] = acc[segment.videoID] || {
hash: segment.hashedVideoID,
segments: []
};
acc[segment.videoID].segments ??= [];
acc[segment.videoID].segments.push(segment);
return acc;
}, {});
for (const [videoID, videoData] of Object.entries(segmentPerVideoID)) {
const data: VideoData = {
hash: videoData.hash,
segments: chooseSegment(videoData.segments),
};
if (data.segments.length > 0) {
segments[videoID] = data;
}
}
return segments;
} catch (err) {
Logger.error(err as string);
return null;
}
}
async function getSegmentsFromDBByHash(hashedVideoIDPrefix: VideoIDHash, service: Service): Promise<DBSegment[]> {
const fetchFromDB = () => db
.prepare(
"all",
`SELECT "startTime", "endTime", "videoID", "votes", "locked", "UUID", "userID", "category", "actionType", "hashedVideoID", "description" FROM "sponsorTimes"
WHERE "hashedVideoID" LIKE ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`,
[`${hashedVideoIDPrefix}%`, service]
) as Promise<DBSegment[]>;
if (hashedVideoIDPrefix.length === 4) {
return await QueryCacher.get(fetchFromDB, videoLabelsHashKey(hashedVideoIDPrefix, service));
}
return await fetchFromDB();
}
async function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
const fetchFromDB = () => db
.prepare(
"all",
`SELECT "startTime", "endTime", "votes", "locked", "UUID", "userID", "category", "actionType", "description" FROM "sponsorTimes"
WHERE "videoID" = ? AND "service" = ? AND "actionType" = 'full' AND "hidden" = 0 AND "shadowHidden" = 0`,
[videoID, service]
) as Promise<DBSegment[]>;
return await QueryCacher.get(fetchFromDB, videoLabelsKey(videoID, service));
}
function chooseSegment<T extends DBSegment>(choices: T[]): Segment[] {
// filter out -2 segments
choices = choices.filter((segment) => segment.votes > -2);
const results = [];
// trivial decisions
if (choices.length === 0) {
return [];
} else if (choices.length === 1) {
return transformDBSegments(choices);
}
// if locked, only choose from locked
const locked = choices.filter((segment) => segment.locked);
if (locked.length > 0) {
choices = locked;
}
//no need to filter, just one label
if (choices.length === 1) {
return transformDBSegments(choices);
}
// sponsor > exclusive > selfpromo
const findCategory = (category: string) => choices.find((segment) => segment.category === category);
const categoryResult = findCategory("sponsor") ?? findCategory("exclusive_access") ?? findCategory("selfpromo");
if (categoryResult) results.push(categoryResult);
return transformDBSegments(results);
}
async function handleGetLabel(req: Request, res: Response): Promise<Segment[] | false> {
const videoID = req.query.videoID as VideoID;
if (!videoID) {
res.status(400).send("videoID not specified");
return false;
}
const service = getService(req.query.service, req.body.service);
const segments = await getLabelsByVideoID(videoID, service);
if (!segments || segments.length === 0) {
res.sendStatus(404);
return false;
}
return segments;
}
async function endpoint(req: Request, res: Response): Promise<Response> {
try {
const segments = await handleGetLabel(req, res);
// If false, res.send has already been called
if (segments) {
//send result
return res.send(segments);
}
} catch (err) {
if (err instanceof SyntaxError) {
return res.status(400).send("Categories parameter does not match format requirements.");
} else return res.sendStatus(500);
}
}
export {
getLabelsByVideoID,
getLabelsByHash,
endpoint
};

View File

@@ -0,0 +1,27 @@
import { hashPrefixTester } from "../utils/hashPrefixTester";
import { getLabelsByHash } from "./getVideoLabel";
import { Request, Response } from "express";
import { VideoIDHash, Service } from "../types/segments.model";
import { getService } from "../utils/getService";
export async function getVideoLabelsByHash(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
if (!req.params.prefix || !hashPrefixTester(req.params.prefix)) {
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
}
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
const service: Service = getService(req.query.service, req.body.service);
// Get all video id's that match hash prefix
const segments = await getLabelsByHash(hashPrefix, service);
if (!segments) return res.status(404).json([]);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
hash: data.hash,
segments: data.segments,
}));
return res.status(output.length === 0 ? 404 : 200).json(output);
}

View File

@@ -25,7 +25,7 @@ export async function getViewsForUser(req: Request, res: Response): Promise<Resp
} else {
return res.sendStatus(404);
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -47,7 +47,7 @@ export async function postClearCache(req: Request, res: Response): Promise<Respo
return res.status(200).json({
message: `Cache cleared on video ${videoID}`
});
} catch(err) {
} catch(err) /* istanbul ignore next */ {
return res.sendStatus(500);
}
}

View File

@@ -37,7 +37,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
if (!userIsVIP) {
res.status(403).json({
message: "Must be a VIP to mark videos.",
message: "Must be a VIP to lock videos.",
});
return;
}
@@ -66,7 +66,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
for (const lock of locksToApply) {
try {
await db.prepare("run", `INSERT INTO "lockCategories" ("videoID", "userID", "actionType", "category", "hashedVideoID", "reason", "service") VALUES(?, ?, ?, ?, ?, ?, ?)`, [videoID, userID, lock.actionType, lock.category, hashedVideoID, reason, service]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
Logger.error(err as string);
res.status(500).json({
@@ -82,7 +82,7 @@ export async function postLockCategories(req: Request, res: Response): Promise<s
await db.prepare("run",
'UPDATE "lockCategories" SET "reason" = ?, "userID" = ? WHERE "videoID" = ? AND "actionType" = ? AND "category" = ? AND "service" = ?',
[reason, userID, videoID, lock.actionType, lock.category, service]);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(`Error submitting 'lockCategories' marker for category '${lock.category}' and actionType '${lock.actionType}' for video '${videoID}' (${service})`);
Logger.error(err as string);
res.status(500).json({

View File

@@ -37,7 +37,7 @@ export async function postPurgeAllSegments(req: Request, res: Response): Promise
service
});
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -91,7 +91,7 @@ export async function postSegmentShift(req: Request, res: Response): Promise<Res
break;
}
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -56,7 +56,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
return res.sendStatus(200);
}
}
catch (error) {
catch (error) /* istanbul ignore next */ {
Logger.error(error as string);
return res.sendStatus(500);
}
@@ -83,7 +83,7 @@ export async function setUsername(req: Request, res: Response): Promise<Response
await logUserNameChange(userID, userName, oldUserName, adminUserIDInput !== undefined);
return res.sendStatus(200);
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
return res.sendStatus(500);
}

View File

@@ -4,7 +4,6 @@ import { config } from "../config";
import { privateDB } from "../databases/databases";
import { Logger } from "../utils/logger";
import { getPatreonIdentity, PatronStatus, refreshToken, TokenType } from "../utils/tokenUtils";
import FormData from "form-data";
interface VerifyTokenRequest extends Request {
query: {
@@ -12,14 +11,16 @@ interface VerifyTokenRequest extends Request {
}
}
export const validatelicenseKeyRegex = (token: string) =>
new RegExp(/[A-Za-z0-9]{40}|[A-Za-z0-9-]{35}/).test(token);
export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response): Promise<Response> {
const { query: { licenseKey } } = req;
if (!licenseKey) {
return res.status(400).send("Invalid request");
}
const licenseRegex = new RegExp(/[a-zA-Z0-9]{40}|[A-Z0-9-]{35}/);
if (!licenseRegex.test(licenseKey)) {
} else if (!validatelicenseKeyRegex(licenseKey)) {
// fast check for invalid licence key
return res.status(200).send({
allowed: false
});
@@ -34,6 +35,7 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
refreshToken(TokenType.patreon, licenseKey, tokens.refreshToken).catch(Logger.error);
}
/* istanbul ignore else */
if (identity) {
const membership = identity.included?.[0]?.attributes;
const allowed = !!membership && ((membership.patron_status === PatronStatus.active && membership.currently_entitled_amount_cents > 0)
@@ -65,20 +67,13 @@ export async function verifyTokenRequest(req: VerifyTokenRequest, res: Response)
async function checkAllGumroadProducts(licenseKey: string): Promise<boolean> {
for (const link of config.gumroad.productPermalinks) {
try {
const formData = new FormData();
formData.append("product_permalink", link);
formData.append("license_key", licenseKey);
const result = await axios.request({
url: "https://api.gumroad.com/v2/licenses/verify",
data: formData,
method: "POST",
headers: formData.getHeaders()
const result = await axios.post("https://api.gumroad.com/v2/licenses/verify", {
params: { product_permalink: link, license_key: licenseKey }
});
const allowed = result.status === 200 && result.data?.success;
if (allowed) return allowed;
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`Gumroad fetch for ${link} failed: ${e}`);
}
}

View File

@@ -5,7 +5,7 @@ import { HashedUserID, UserID } from "./user.model";
export type SegmentUUID = string & { __segmentUUIDBrand: unknown };
export type VideoID = string & { __videoIDBrand: unknown };
export type VideoDuration = number & { __videoDurationBrand: unknown };
export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "filler" | "poi_highlight" | "chapter") & { __categoryBrand: unknown };
export type Category = ("sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "poi_highlight" | "chapter" | "filler" | "exclusive_access") & { __categoryBrand: unknown };
export type VideoIDHash = VideoID & HashedValue;
export type IPAddress = string & { __ipAddressBrand: unknown };
export type HashedIP = IPAddress & HashedValue;

View File

@@ -3,6 +3,9 @@ import { Request } from "express";
import { IPAddress } from "../types/segments.model";
export function getIP(req: Request): IPAddress {
// if in testing mode, return immediately
if (config.mode === "test") return "127.0.0.1" as IPAddress;
if (config.behindProxy === true || config.behindProxy === "true") {
config.behindProxy = "X-Forwarded-For";
}
@@ -15,6 +18,6 @@ export function getIP(req: Request): IPAddress {
case "X-Real-IP":
return req.headers["x-real-ip"] as IPAddress;
default:
return (req.connection?.remoteAddress || req.socket?.remoteAddress) as IPAddress;
return req.socket?.remoteAddress as IPAddress;
}
}

View File

@@ -18,6 +18,7 @@ async function getFromITube (videoID: string): Promise<innerTubeVideoDetails> {
const result = await axios.post(url, data, {
timeout: 3500
});
/* istanbul ignore else */
if (result.status === 200) {
return result.data.videoDetails;
} else {
@@ -39,6 +40,7 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom
return data as innerTubeVideoDetails;
}
} catch (err) {
/* istanbul ignore next */
return Promise.reject(err);
}
}

View File

@@ -1,6 +1,6 @@
import redis from "../utils/redis";
import { Logger } from "../utils/logger";
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey } from "./redisKeys";
import { skipSegmentsHashKey, skipSegmentsKey, reputationKey, ratingHashKey, skipSegmentGroupsKey, userFeatureKey, videoLabelsKey, videoLabelsHashKey } from "./redisKeys";
import { Service, VideoID, VideoIDHash } from "../types/segments.model";
import { Feature, HashedUserID, UserID } from "../types/user.model";
import { config } from "../config";
@@ -81,6 +81,8 @@ function clearSegmentCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoID
redis.del(skipSegmentsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(skipSegmentGroupsKey(videoInfo.videoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(videoLabelsKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
redis.del(videoLabelsHashKey(videoInfo.hashedVideoID, videoInfo.service)).catch((err) => Logger.error(err));
if (videoInfo.userID) redis.del(reputationKey(videoInfo.userID)).catch((err) => Logger.error(err));
}
}

View File

@@ -38,6 +38,16 @@ export function shaHashKey(singleIter: HashedValue): string {
export const tempVIPKey = (userID: HashedUserID): string =>
`vip.temp.${userID}`;
export const videoLabelsKey = (videoID: VideoID, service: Service): string =>
`labels.v1.${service}.videoID.${videoID}`;
export function videoLabelsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string {
hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash;
if (hashedVideoIDPrefix.length !== 4) Logger.warn(`Redis skip segment hash-prefix key is not length 4! ${hashedVideoIDPrefix}`);
return `labels.v1.${service}.${hashedVideoIDPrefix}`;
}
export function userFeatureKey (userID: HashedUserID, feature: Feature): string {
return `user.${userID}.feature.${feature}`;
}

View File

@@ -58,12 +58,11 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis
return licenseKey;
}
} catch (e) {
break;
} catch (e) /* istanbul ignore next */ {
Logger.error(`token creation: ${e}`);
return null;
}
break;
}
case TokenType.local: {
const licenseKey = generateToken();
@@ -74,7 +73,6 @@ export async function createAndSaveToken(type: TokenType, code?: string): Promis
return licenseKey;
}
}
return null;
}
@@ -102,15 +100,12 @@ export async function refreshToken(type: TokenType, licenseKey: string, refreshT
return true;
}
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`token refresh: ${e}`);
return false;
}
break;
}
}
return false;
}
@@ -136,9 +131,8 @@ export async function getPatreonIdentity(accessToken: string): Promise<PatreonId
if (identityRequest.status === 200) {
return identityRequest.data;
}
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`identity request: ${e}`);
}
return null;
}