From 939ec61c0eaba3d12474950618eee3225f866fbe Mon Sep 17 00:00:00 2001 From: Ajay Date: Wed, 29 Dec 2021 00:18:07 -0500 Subject: [PATCH 1/7] Increase threshold for category overlap --- src/routes/getSkipSegments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/getSkipSegments.ts b/src/routes/getSkipSegments.ts index 7850661..aba5e40 100644 --- a/src/routes/getSkipSegments.ts +++ b/src/routes/getSkipSegments.ts @@ -307,7 +307,7 @@ function splitPercentOverlap(groups: OverlappingSegmentGroup[]): OverlappingSegm const overlapPercent = overlap / overallDuration; return (overlapPercent > 0 && segment.actionType === compareSegment.actionType && segment.category === compareSegment.category && segment.actionType !== ActionType.Chapter) || (overlapPercent >= 0.6 && segment.actionType !== compareSegment.actionType && segment.category === compareSegment.category) - || (overlapPercent >= 0.8 && segment.actionType === compareSegment.actionType && segment.category !== compareSegment.category + || (overlapPercent >= 0.95 && segment.actionType === compareSegment.actionType && segment.category !== compareSegment.category && segment.category !== "music_offtopic" && compareSegment.category !== "music_offtopic") || (overlapPercent >= 0.8 && segment.actionType === ActionType.Chapter && compareSegment.actionType === ActionType.Chapter); }); From 13a4bc3ee98df5398016906ab249adbfac944521 Mon Sep 17 00:00:00 2001 From: Ajay Date: Wed, 29 Dec 2021 00:19:53 -0500 Subject: [PATCH 2/7] Fix tests --- test/cases/getSkipSegmentsByHash.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cases/getSkipSegmentsByHash.ts b/test/cases/getSkipSegmentsByHash.ts index e889238..7cf2081 100644 --- a/test/cases/getSkipSegmentsByHash.ts +++ b/test/cases/getSkipSegmentsByHash.ts @@ -47,9 +47,9 @@ describe("getSkipSegmentsByHash", () => { await db.prepare("run", query, ["requiredSegmentHashVid", 10, 20, -2, 0, "fbf0af454059733c8822f6a4ac8ec568e0787f8c0a5ee915dd5b05e0d7a9a388", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentHashVidHash, ""]); await db.prepare("run", query, ["requiredSegmentHashVid", 20, 30, -2, 0, "7e1ebc5194551d2d0a606d64f675e5a14952e4576b2959f8c9d51e316c14f8da", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, requiredSegmentHashVidHash, ""]); await db.prepare("run", query, ["differentCategoryVid", 60, 70, 2, 0, "differentCategoryVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, differentCategoryVidHash, ""]); - await db.prepare("run", query, ["differentCategoryVid", 61, 70, 2, 1, "differentCategoryVid-2", "testman", 0, 50, "intro", "skip", "YouTube", 0, 0, differentCategoryVidHash, ""]); + await db.prepare("run", query, ["differentCategoryVid", 60, 70, 2, 1, "differentCategoryVid-2", "testman", 0, 50, "intro", "skip", "YouTube", 0, 0, differentCategoryVidHash, ""]); await db.prepare("run", query, ["nonMusicOverlapVid", 60, 70, 2, 0, "nonMusicOverlapVid-1", "testman", 0, 50, "sponsor", "skip", "YouTube", 0, 0, nonMusicOverlapVidHash, ""]); - await db.prepare("run", query, ["nonMusicOverlapVid", 61, 70, 2, 1, "nonMusicOverlapVid-2", "testman", 0, 50, "music_offtopic", "skip", "YouTube", 0, 0, nonMusicOverlapVidHash, ""]); + await db.prepare("run", query, ["nonMusicOverlapVid", 60, 70, 2, 1, "nonMusicOverlapVid-2", "testman", 0, 50, "music_offtopic", "skip", "YouTube", 0, 0, nonMusicOverlapVidHash, ""]); }); it("Should be able to get a 200", (done) => { From 00d7e1f0588de0eceafa79d90de3aa08d2b54f6f Mon Sep 17 00:00:00 2001 From: Ajay Date: Wed, 29 Dec 2021 00:22:02 -0500 Subject: [PATCH 3/7] Force skip segments cache clear --- src/utils/redisKeys.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/redisKeys.ts b/src/utils/redisKeys.ts index 65b12ff..4dbd1ec 100644 --- a/src/utils/redisKeys.ts +++ b/src/utils/redisKeys.ts @@ -4,18 +4,18 @@ import { HashedValue } from "../types/hash.model"; import { Logger } from "./logger"; export function skipSegmentsKey(videoID: VideoID, service: Service): string { - return `segments.v2.${service}.videoID.${videoID}`; + return `segments.v3.${service}.videoID.${videoID}`; } export function skipSegmentGroupsKey(videoID: VideoID, service: Service): string { - return `segments.groups.${service}.videoID.${videoID}`; + return `segments.groups.v2.${service}.videoID.${videoID}`; } export function skipSegmentsHashKey(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 `segments.v2.${service}.${hashedVideoIDPrefix}`; + return `segments.v3.${service}.${hashedVideoIDPrefix}`; } export function reputationKey(userID: UserID): string { From ceabeefe212706d59b1dd6f6e806a08cd9ef1493 Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Wed, 29 Dec 2021 23:33:00 +0100 Subject: [PATCH 4/7] disable logging again --- nginx/nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index f0624d9..d157b32 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -26,6 +26,8 @@ http { keepalive_timeout 5; proxy_http_version 1.1; proxy_set_header Connection ""; + access_log off; + error_log /dev/null crit; upstream backend_GET { ip_hash; From b9a620fc3bfcf0fa5288f000163364cba5e7a6ac Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 30 Dec 2021 16:47:11 -0500 Subject: [PATCH 5/7] add redis status/min --- src/routes/getStatus.ts | 6 +++++- src/utils/redis.ts | 10 +++++++++- test/cases/getStatus.ts | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/routes/getStatus.ts b/src/routes/getStatus.ts index b4ea254..bc855c2 100644 --- a/src/routes/getStatus.ts +++ b/src/routes/getStatus.ts @@ -2,6 +2,7 @@ import { db } from "../databases/databases"; import { Logger } from "../utils/logger"; import { Request, Response } from "express"; import os from "os"; +import redis from "../utils/redis"; export async function getStatus(req: Request, res: Response): Promise { const startTime = Date.now(); @@ -9,13 +10,16 @@ export async function getStatus(req: Request, res: Response): Promise value = Array.isArray(value) ? value[0] : value; try { const dbVersion = (await db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"])).value; + const numberRequests = await redis.increment("statusRequest"); + const statusRequests = numberRequests?.replies?.[0]; const statusValues: Record = { uptime: process.uptime(), commit: (global as any).HEADCOMMIT || "unknown", db: Number(dbVersion), startTime, processTime: Date.now() - startTime, - loadavg: os.loadavg().slice(1) // only return 5 & 15 minute load average + loadavg: os.loadavg().slice(1), // only return 5 & 15 minute load average + statusRequests }; return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues); } catch (err) { diff --git a/src/utils/redis.ts b/src/utils/redis.ts index 8bdea23..6e4d1d6 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -9,6 +9,7 @@ interface RedisSB { setAsync?(key: string, value: string): Promise<{err: Error | null, reply: string | null}>; delAsync?(...keys: [string]): Promise; close?(flush?: boolean): void; + increment?(key: string): Promise<{err: Error| null, replies: any[] | null}>; } let exportObject: RedisSB = { @@ -20,6 +21,8 @@ let exportObject: RedisSB = { new Promise((resolve) => resolve({ err: null, reply: undefined })), delAsync: () => new Promise((resolve) => resolve(null)), + increment: () => + new Promise((resolve) => resolve({ err: null, replies: undefined })), }; if (config.redis) { @@ -31,7 +34,12 @@ if (config.redis) { exportObject.setAsync = (key, value) => new Promise((resolve) => client.set(key, value, (err, reply) => resolve({ err, reply }))); exportObject.delAsync = (...keys) => new Promise((resolve) => client.del(keys, (err) => resolve(err))); exportObject.close = (flush) => client.end(flush); - + exportObject.increment = (key) => new Promise((resolve) => + client.multi() + .incr(key) + .expire(key, 60) + .exec((err, replies) => resolve({ err, replies })) + ); client.on("error", function(error) { Logger.error(error); }); diff --git a/test/cases/getStatus.ts b/test/cases/getStatus.ts index b515427..970ebed 100644 --- a/test/cases/getStatus.ts +++ b/test/cases/getStatus.ts @@ -1,6 +1,7 @@ import assert from "assert"; import { db } from "../../src/databases/databases"; import { client } from "../utils/httpClient"; +import { config } from "../../src/config"; let dbVersion: number; describe("getStatus", () => { @@ -86,4 +87,27 @@ describe("getStatus", () => { }) .catch(err => done(err)); }); + + it("Should be able to get statusRequests only", function (done) { + if (!config.redis) this.skip(); + client.get(`${endpoint}/statusRequests`) + .then(res => { + assert.strictEqual(res.status, 200); + assert.ok(Number(res.data) > 1); + done(); + }) + .catch(err => done(err)); + }); + + it("Should be able to get status with statusRequests", function (done) { + if (!config.redis) this.skip(); + client.get(endpoint) + .then(res => { + assert.strictEqual(res.status, 200); + const data = res.data; + assert.ok(data.statusRequests > 2); + done(); + }) + .catch(err => done(err)); + }); }); From 9ae16ea9b6154771a420497e9460641528832d47 Mon Sep 17 00:00:00 2001 From: Michael C Date: Thu, 30 Dec 2021 22:21:27 -0500 Subject: [PATCH 6/7] add tempVIP check to vote --- src/routes/voteOnSponsorTime.ts | 21 +++++++++++++++++---- src/utils/redisKeys.ts | 20 ++++++++++---------- src/utils/webhookUtils.ts | 8 ++++++-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 582be78..fc0940d 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -9,11 +9,13 @@ 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 { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, Visibility, VideoDuration } from "../types/segments.model"; import { getCategoryActionType } from "../utils/categoryInfo"; import { QueryCacher } from "../utils/queryCacher"; import axios from "axios"; +import redis from "../utils/redis"; +import { tempVIPKey } from "../utils/redisKeys"; const voteTypes = { normal: 0, @@ -37,6 +39,7 @@ interface VoteData { UUID: string; nonAnonUserID: string; voteTypeEnum: number; + isTempVIP: boolean; isVIP: boolean; isOwnSubmission: boolean; row: { @@ -57,6 +60,13 @@ function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise => { + const apiVideoInfo = await getYouTubeVideoInfo(videoID); + const channelID = apiVideoInfo?.data?.authorId; + const { err, reply } = await redis.getAsync(tempVIPKey(nonAnonUserID)); + return err ? false : (reply == channelID); +}; + const videoDurationChanged = (segmentDuration: number, APIDuration: number) => (APIDuration > 0 && Math.abs(segmentDuration - APIDuration) > 2); async function checkVideoDurationChange(UUID: SegmentUUID) { @@ -105,7 +115,7 @@ async function sendWebhooks(voteData: VoteData) { // Send custom webhooks dispatchEvent(isUpvote ? "vote.up" : "vote.down", { "user": { - "status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission), + "status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isTempVIP, voteData.isVIP, voteData.isOwnSubmission), }, "video": { "id": submissionInfoRow.videoID, @@ -153,7 +163,7 @@ async function sendWebhooks(voteData: VoteData) { "author": { "name": voteData.finalResponse?.webhookMessage ?? voteData.finalResponse?.finalMessage ?? - getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission), + getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isTempVIP, voteData.isVIP, voteData.isOwnSubmission), }, "thumbnail": { "url": getMaxResThumbnail(data) || "", @@ -311,7 +321,9 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise + `segments.v3.${service}.videoID.${videoID}`; -export function skipSegmentGroupsKey(videoID: VideoID, service: Service): string { - return `segments.groups.v2.${service}.videoID.${videoID}`; -} +export const skipSegmentGroupsKey = (videoID: VideoID, service: Service): string => + `segments.groups.v2.${service}.videoID.${videoID}`; export function skipSegmentsHashKey(hashedVideoIDPrefix: VideoIDHash, service: Service): string { hashedVideoIDPrefix = hashedVideoIDPrefix.substring(0, 4) as VideoIDHash; @@ -18,9 +16,8 @@ export function skipSegmentsHashKey(hashedVideoIDPrefix: VideoIDHash, service: S return `segments.v3.${service}.${hashedVideoIDPrefix}`; } -export function reputationKey(userID: UserID): string { - return `reputation.user.${userID}`; -} +export const reputationKey = (userID: UserID): string => + `reputation.user.${userID}`; export function ratingHashKey(hashPrefix: VideoIDHash, service: Service): string { hashPrefix = hashPrefix.substring(0, 4) as VideoIDHash; @@ -33,4 +30,7 @@ export function shaHashKey(singleIter: HashedValue): string { if (singleIter.length !== 64) Logger.warn(`Redis sha.hash key is not length 64! ${singleIter}`); return `sha.hash.${singleIter}`; -} \ No newline at end of file +} + +export const tempVIPKey = (userID: UserID): string => + `vip.temp.${userID}`; \ No newline at end of file diff --git a/src/utils/webhookUtils.ts b/src/utils/webhookUtils.ts index c352bf9..67e2817 100644 --- a/src/utils/webhookUtils.ts +++ b/src/utils/webhookUtils.ts @@ -2,9 +2,11 @@ import { config } from "../config"; import { Logger } from "../utils/logger"; import axios from "axios"; -function getVoteAuthorRaw(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string { +function getVoteAuthorRaw(submissionCount: number, isTempVIP: boolean, isVIP: boolean, isOwnSubmission: boolean): string { if (isOwnSubmission) { return "self"; + } else if (isTempVIP) { + return "temp vip"; } else if (isVIP) { return "vip"; } else if (submissionCount === 0) { @@ -14,11 +16,13 @@ function getVoteAuthorRaw(submissionCount: number, isVIP: boolean, isOwnSubmissi } } -function getVoteAuthor(submissionCount: number, isVIP: boolean, isOwnSubmission: boolean): string { +function getVoteAuthor(submissionCount: number, isTempVIP: boolean, isVIP: boolean, isOwnSubmission: boolean): string { if (submissionCount === 0) { return "Report by New User"; } else if (isOwnSubmission) { return "Report by Submitter"; + } else if (isTempVIP) { + return "Report by Temp VIP"; } else if (isVIP) { return "Report by VIP User"; } From a1d28fbfe118d4f6eadd5494d254cb74e05b7851 Mon Sep 17 00:00:00 2001 From: Michael C Date: Fri, 31 Dec 2021 04:26:37 -0500 Subject: [PATCH 7/7] add addUserAsTempVIP --- DatabaseSchema.md | 9 ++ databases/_upgrade_private_4.sql | 2 +- databases/_upgrade_private_5.sql | 12 +++ src/app.ts | 3 + src/routes/addUserAsTempVIP.ts | 71 +++++++++++++ src/routes/voteOnSponsorTime.ts | 2 +- src/utils/redis.ts | 4 + src/utils/redisKeys.ts | 4 +- test/cases/tempVip.ts | 165 +++++++++++++++++++++++++++++++ test/youtubeMock.ts | 9 ++ 10 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 databases/_upgrade_private_5.sql create mode 100644 src/routes/addUserAsTempVIP.ts create mode 100644 test/cases/tempVip.ts diff --git a/DatabaseSchema.md b/DatabaseSchema.md index bcd9949..f0d0785 100644 --- a/DatabaseSchema.md +++ b/DatabaseSchema.md @@ -207,6 +207,7 @@ [categoryVotes](#categoryVotes) [sponsorTimes](#sponsorTimes) [config](#config) +[tempVipLog](#tempVipLog) ### vote @@ -270,3 +271,11 @@ | index | field | | -- | :--: | | ratings_videoID | videoID, service, userID, timeSubmitted | + +### tempVipLog +| Name | Type | | +| -- | :--: | -- | +| issuerUserID | TEXT | not null | +| targetUserID | TEXT | not null | +| enabled | BOOLEAN | not null | +| updatedAt | INTEGER | not null | \ No newline at end of file diff --git a/databases/_upgrade_private_4.sql b/databases/_upgrade_private_4.sql index 1bc137c..008cb67 100644 --- a/databases/_upgrade_private_4.sql +++ b/databases/_upgrade_private_4.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS "ratings" ( "service" TEXT NOT NULL default 'YouTube', "type" INTEGER NOT NULL, "userID" TEXT NOT NULL, - "timeSubmitted" INTEGER NOT NULL, + "timeSubmitted" INTEGER NOT NULL, "hashedIP" TEXT NOT NULL ); diff --git a/databases/_upgrade_private_5.sql b/databases/_upgrade_private_5.sql new file mode 100644 index 0000000..3d98aee --- /dev/null +++ b/databases/_upgrade_private_5.sql @@ -0,0 +1,12 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS "tempVipLog" ( + "issuerUserID" TEXT NOT NULL, + "targetUserID" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL, + "updatedAt" INTEGER NOT NULL +); + +UPDATE "config" SET value = 5 WHERE key = 'version'; + +COMMIT; \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 23ed171..3c84724 100644 --- a/src/app.ts +++ b/src/app.ts @@ -46,6 +46,7 @@ import { getChapterNames } from "./routes/getChapterNames"; import { postRating } from "./routes/ratings/postRating"; import { getRating } from "./routes/ratings/getRating"; import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache"; +import { addUserAsTempVIP } from "./routes/addUserAsTempVIP"; export function createServer(callback: () => void): Server { // Create a service (the app object is just a callback). @@ -117,6 +118,8 @@ function setupRoutes(router: Router) { //Endpoint used to make a user a VIP user with special privileges router.post("/api/addUserAsVIP", addUserAsVIP); + //Endpoint to add a user as a temporary VIP + router.post("/api/addUserAsTempVIP", addUserAsTempVIP); //Gets all the views added up for one userID //Useful to see how much one user has contributed diff --git a/src/routes/addUserAsTempVIP.ts b/src/routes/addUserAsTempVIP.ts new file mode 100644 index 0000000..6788fa2 --- /dev/null +++ b/src/routes/addUserAsTempVIP.ts @@ -0,0 +1,71 @@ +import { VideoID } from "../types/segments.model"; +import { YouTubeAPI } from "../utils/youtubeApi"; +import { APIVideoInfo } from "../types/youtubeApi.model"; +import { config } from "../config"; +import { getHashCache } from "../utils/getHashCache"; +import { privateDB } from "../databases/databases"; +import { Request, Response } from "express"; +import { isUserVIP } from "../utils/isUserVIP"; +import { HashedUserID } from "../types/user.model"; +import redis from "../utils/redis"; +import { tempVIPKey } from "../utils/redisKeys"; + +interface AddUserAsTempVIPRequest extends Request { + query: { + userID: HashedUserID; + adminUserID: string; + enabled: string; + channelVideoID: string; + } +} + +function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise { + return (config.newLeafURLs) ? YouTubeAPI.listVideos(videoID, ignoreCache) : null; +} + +const getChannelInfo = async (videoID: VideoID): Promise<{id: string | null, name: string | null }> => { + const videoInfo = await getYouTubeVideoInfo(videoID); + return { + id: videoInfo?.data?.authorId, + name: videoInfo?.data?.author + }; +}; + +export async function addUserAsTempVIP(req: AddUserAsTempVIPRequest, res: Response): Promise { + const { query: { userID, adminUserID } } = req; + + const enabled = req.query?.enabled === "true"; + const channelVideoID = req.query?.channelVideoID as VideoID; + + if (!userID || !adminUserID || !channelVideoID ) { + // invalid request + return res.sendStatus(400); + } + + // hash the issuer userID + const issuerUserID = await getHashCache(adminUserID); + // check if issuer is VIP + const issuerIsVIP = await isUserVIP(issuerUserID as HashedUserID); + if (!issuerIsVIP) { + return res.sendStatus(403); + } + + // check to see if this user is already a vip + const targetIsVIP = await isUserVIP(userID); + if (targetIsVIP) { + return res.sendStatus(409); + } + + const startTime = Date.now(); + const dayInSeconds = 86400; + const channelInfo = await getChannelInfo(channelVideoID); + + await privateDB.prepare("run", `INSERT INTO "tempVipLog" VALUES (?, ?, ?, ?)`, [adminUserID, userID, + enabled, startTime]); + if (enabled) { // add to redis + await redis.setAsyncEx(tempVIPKey(userID), channelInfo?.id, dayInSeconds); + } else { // delete key + await redis.delAsync(tempVIPKey(userID)); + } + + return res.sendStatus(200); +} \ No newline at end of file diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index fc0940d..ec70c11 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -322,7 +322,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise; set(key: string, value: string, callback?: Callback): void; setAsync?(key: string, value: string): Promise<{err: Error | null, reply: string | null}>; + setAsyncEx?(key: string, value: string, seconds: number): Promise<{err: Error | null, reply: string | null}>; delAsync?(...keys: [string]): Promise; close?(flush?: boolean): void; } @@ -18,6 +19,8 @@ let exportObject: RedisSB = { set: (key, value, callback) => callback(null, undefined), setAsync: () => new Promise((resolve) => resolve({ err: null, reply: undefined })), + setAsyncEx: () => + new Promise((resolve) => resolve({ err: null, reply: undefined })), delAsync: () => new Promise((resolve) => resolve(null)), }; @@ -29,6 +32,7 @@ if (config.redis) { exportObject.getAsync = (key) => new Promise((resolve) => client.get(key, (err, reply) => resolve({ err, reply }))); exportObject.setAsync = (key, value) => new Promise((resolve) => client.set(key, value, (err, reply) => resolve({ err, reply }))); + exportObject.setAsyncEx = (key, value, seconds) => new Promise((resolve) => client.setex(key, seconds, value, (err, reply) => resolve({ err, reply }))); exportObject.delAsync = (...keys) => new Promise((resolve) => client.del(keys, (err) => resolve(err))); exportObject.close = (flush) => client.end(flush); diff --git a/src/utils/redisKeys.ts b/src/utils/redisKeys.ts index 7afba40..38e675e 100644 --- a/src/utils/redisKeys.ts +++ b/src/utils/redisKeys.ts @@ -1,5 +1,5 @@ import { Service, VideoID, VideoIDHash } from "../types/segments.model"; -import { UserID } from "../types/user.model"; +import { HashedUserID, UserID } from "../types/user.model"; import { HashedValue } from "../types/hash.model"; import { Logger } from "./logger"; @@ -32,5 +32,5 @@ export function shaHashKey(singleIter: HashedValue): string { return `sha.hash.${singleIter}`; } -export const tempVIPKey = (userID: UserID): string => +export const tempVIPKey = (userID: HashedUserID): string => `vip.temp.${userID}`; \ No newline at end of file diff --git a/test/cases/tempVip.ts b/test/cases/tempVip.ts new file mode 100644 index 0000000..a054a12 --- /dev/null +++ b/test/cases/tempVip.ts @@ -0,0 +1,165 @@ +import { config } from "../../src/config"; +import { getHash } from "../../src/utils/getHash"; +import { tempVIPKey } from "../../src/utils/redisKeys"; +import { HashedUserID } from "../../src/types/user.model"; +import { client } from "../utils/httpClient"; +import { db, privateDB } from "../../src/databases/databases"; +import redis from "../../src/utils/redis"; +import assert from "assert"; + +// helpers +const getSegment = (UUID: string) => db.prepare("get", `SELECT "votes", "locked", "category" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]); + +const permVIP = "tempVipPermOne"; +const publicPermVIP = getHash(permVIP) as HashedUserID; +const tempVIPOne = "tempVipTempOne"; +const publicTempVIPOne = getHash(tempVIPOne) as HashedUserID; +const UUID0 = "tempvip-uuid0"; +const UUID1 = "tempvip-uuid1"; + +const tempVIPEndpoint = "/api/addUserAsTempVIP"; +const addTempVIP = (enabled: boolean) => client({ + url: tempVIPEndpoint, + method: "POST", + params: { + userID: publicTempVIPOne, + adminUserID: permVIP, + channelVideoID: "channelid-convert", + enabled: enabled + } +}); +const voteEndpoint = "/api/voteOnSponsorTime"; +const postVote = (userID: string, UUID: string, type: number) => client({ + method: "POST", + url: voteEndpoint, + params: { + userID, + UUID, + type + } +}); +const postVoteCategory = (userID: string, UUID: string, category: string) => client({ + method: "POST", + url: voteEndpoint, + params: { + userID, + UUID, + category + } +}); +const checkUserVIP = async () => { + const { reply } = await redis.getAsync(tempVIPKey(publicTempVIPOne)); + return reply; +}; + +describe("tempVIP test", function() { + before(async function() { + if (!config.redis) this.skip(); + + const insertSponsorTimeQuery = 'INSERT INTO "sponsorTimes" ("videoID", "startTime", "endTime", "votes", "locked", "UUID", "userID", "timeSubmitted", "views", "category", "shadowHidden") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + await db.prepare("run", insertSponsorTimeQuery, ["channelid-convert", 0, 1, 0, 0, UUID0, "testman", 0, 50, "sponsor", 0]); + await db.prepare("run", insertSponsorTimeQuery, ["channelid-convert", 1, 9, 0, 1, "tempvip-submit", publicTempVIPOne, 0, 50, "sponsor", 0]); + await db.prepare("run", insertSponsorTimeQuery, ["otherchannel", 1, 9, 0, 1, UUID1, "testman", 0, 50, "sponsor", 0]); + + + await db.prepare("run", 'INSERT INTO "vipUsers" ("userID") VALUES (?)', [publicPermVIP]); + // clear redis if running consecutive tests + await redis.delAsync(tempVIPKey(publicTempVIPOne)); + }); + + it("Should update db version when starting the application", () => { + privateDB.prepare("get", "SELECT key, value FROM config where key = ?", ["version"]) + .then(row => { + assert.ok(row.value >= 5, `Versions are not at least 5. private is ${row.value}`); + }); + }); + it("User should not already be temp VIP", (done) => { + checkUserVIP() + .then(result => { + assert.ok(!result); + done(result); + }) + .catch(err => done(err)); + }); + it("Should be able to normal upvote as a user", (done) => { + postVote(tempVIPOne, UUID0, 1) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegment(UUID0); + assert.strictEqual(row.votes, 1); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to add tempVIP", (done) => { + addTempVIP(true) + .then(async res => { + assert.strictEqual(res.status, 200); + const vip = await checkUserVIP(); + assert.ok(vip == "ChannelID"); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to VIP downvote", (done) => { + postVote(tempVIPOne, UUID0, 0) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegment(UUID0); + assert.strictEqual(row.votes, -2); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to VIP lock", (done) => { + postVote(tempVIPOne, UUID0, 1) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegment(UUID0); + assert.ok(row.votes > -2); + assert.strictEqual(row.locked, 1); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to VIP change category", (done) => { + postVoteCategory(tempVIPOne, UUID0, "filler") + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegment(UUID0); + assert.strictEqual(row.category, "filler"); + assert.strictEqual(row.locked, 1); + done(); + }) + .catch(err => done(err)); + }); + it("Should be able to remove tempVIP prematurely", (done) => { + addTempVIP(false) + .then(async res => { + assert.strictEqual(res.status, 200); + const vip = await checkUserVIP(); + done(vip); + }) + .catch(err => done(err)); + }); + it("Should not be able to VIP downvote", (done) => { + postVote(tempVIPOne, UUID1, 0) + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegment(UUID1); + assert.strictEqual(row.votes, 0); + done(); + }) + .catch(err => done(err)); + }); + it("Should not be able to VIP change category", (done) => { + postVoteCategory(tempVIPOne, UUID1, "filler") + .then(async res => { + assert.strictEqual(res.status, 200); + const row = await getSegment(UUID1); + assert.strictEqual(row.category, "sponsor"); + done(); + }) + .catch(err => done(err)); + }); +}); \ No newline at end of file diff --git a/test/youtubeMock.ts b/test/youtubeMock.ts index bb221e0..92ad482 100644 --- a/test/youtubeMock.ts +++ b/test/youtubeMock.ts @@ -47,6 +47,15 @@ export class YouTubeApiMock { ] } as APIVideoData }; + } else if (obj.id === "channelid-convert") { + return { + err: null, + data: { + title: "Video Lookup Title", + author: "ChannelAuthor", + authorId: "ChannelID" + } as APIVideoData + }; } else { return { err: null,