add addUserAsTempVIP

This commit is contained in:
Michael C
2021-12-31 04:26:37 -05:00
parent 9ae16ea9b6
commit a1d28fbfe1
10 changed files with 277 additions and 4 deletions

View File

@@ -207,6 +207,7 @@
[categoryVotes](#categoryVotes) [categoryVotes](#categoryVotes)
[sponsorTimes](#sponsorTimes) [sponsorTimes](#sponsorTimes)
[config](#config) [config](#config)
[tempVipLog](#tempVipLog)
### vote ### vote
@@ -270,3 +271,11 @@
| index | field | | index | field |
| -- | :--: | | -- | :--: |
| ratings_videoID | videoID, service, userID, timeSubmitted | | 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 |

View File

@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS "ratings" (
"service" TEXT NOT NULL default 'YouTube', "service" TEXT NOT NULL default 'YouTube',
"type" INTEGER NOT NULL, "type" INTEGER NOT NULL,
"userID" TEXT NOT NULL, "userID" TEXT NOT NULL,
"timeSubmitted" INTEGER NOT NULL, "timeSubmitted" INTEGER NOT NULL,
"hashedIP" TEXT NOT NULL "hashedIP" TEXT NOT NULL
); );

View File

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

View File

@@ -46,6 +46,7 @@ import { getChapterNames } from "./routes/getChapterNames";
import { postRating } from "./routes/ratings/postRating"; import { postRating } from "./routes/ratings/postRating";
import { getRating } from "./routes/ratings/getRating"; import { getRating } from "./routes/ratings/getRating";
import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache"; import { postClearCache as ratingPostClearCache } from "./routes/ratings/postClearCache";
import { addUserAsTempVIP } from "./routes/addUserAsTempVIP";
export function createServer(callback: () => void): Server { export function createServer(callback: () => void): Server {
// Create a service (the app object is just a callback). // 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 //Endpoint used to make a user a VIP user with special privileges
router.post("/api/addUserAsVIP", addUserAsVIP); 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 //Gets all the views added up for one userID
//Useful to see how much one user has contributed //Useful to see how much one user has contributed

View File

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

View File

@@ -322,7 +322,7 @@ export async function voteOnSponsorTime(req: Request, res: Response): Promise<Re
//check if this user is on the vip list //check if this user is on the vip list
const videoID = await db.prepare("get", `select "videoID" from "sponsorTimes" where "UUID" = ?`, [UUID]); const videoID = await db.prepare("get", `select "videoID" from "sponsorTimes" where "UUID" = ?`, [UUID]);
const isTempVIP = await isUserTempVIP(nonAnonUserID, videoID); const isTempVIP = await isUserTempVIP(nonAnonUserID, videoID?.videoID || null);
const isVIP = await isUserVIP(nonAnonUserID) || isTempVIP; const isVIP = await isUserVIP(nonAnonUserID) || isTempVIP;
//check if user voting on own submission //check if user voting on own submission

View File

@@ -7,6 +7,7 @@ interface RedisSB {
getAsync?(key: string): Promise<{err: Error | null, reply: string | null}>; getAsync?(key: string): Promise<{err: Error | null, reply: string | null}>;
set(key: string, value: string, callback?: Callback<string | null>): void; set(key: string, value: string, callback?: Callback<string | null>): void;
setAsync?(key: string, value: string): Promise<{err: Error | null, reply: string | null}>; 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<Error | null>; delAsync?(...keys: [string]): Promise<Error | null>;
close?(flush?: boolean): void; close?(flush?: boolean): void;
} }
@@ -18,6 +19,8 @@ let exportObject: RedisSB = {
set: (key, value, callback) => callback(null, undefined), set: (key, value, callback) => callback(null, undefined),
setAsync: () => setAsync: () =>
new Promise((resolve) => resolve({ err: null, reply: undefined })), new Promise((resolve) => resolve({ err: null, reply: undefined })),
setAsyncEx: () =>
new Promise((resolve) => resolve({ err: null, reply: undefined })),
delAsync: () => delAsync: () =>
new Promise((resolve) => resolve(null)), 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.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.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.delAsync = (...keys) => new Promise((resolve) => client.del(keys, (err) => resolve(err)));
exportObject.close = (flush) => client.end(flush); exportObject.close = (flush) => client.end(flush);

View File

@@ -1,5 +1,5 @@
import { Service, VideoID, VideoIDHash } from "../types/segments.model"; 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 { HashedValue } from "../types/hash.model";
import { Logger } from "./logger"; import { Logger } from "./logger";
@@ -32,5 +32,5 @@ export function shaHashKey(singleIter: HashedValue): string {
return `sha.hash.${singleIter}`; return `sha.hash.${singleIter}`;
} }
export const tempVIPKey = (userID: UserID): string => export const tempVIPKey = (userID: HashedUserID): string =>
`vip.temp.${userID}`; `vip.temp.${userID}`;

165
test/cases/tempVip.ts Normal file
View File

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

View File

@@ -47,6 +47,15 @@ export class YouTubeApiMock {
] ]
} as APIVideoData } as APIVideoData
}; };
} else if (obj.id === "channelid-convert") {
return {
err: null,
data: {
title: "Video Lookup Title",
author: "ChannelAuthor",
authorId: "ChannelID"
} as APIVideoData
};
} else { } else {
return { return {
err: null, err: null,