Skip to content

Commit

Permalink
Merge pull request #541 from mchangrh/etagTest
Browse files Browse the repository at this point in the history
add etag and other tests
  • Loading branch information
ajayyy authored Feb 22, 2023
2 parents f4286b1 + 436e75e commit d76ee7c
Show file tree
Hide file tree
Showing 42 changed files with 1,626 additions and 1,160 deletions.
1 change: 0 additions & 1 deletion ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
]
}
],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24,
"rateLimit": {
"vote": {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"cover:report": "nyc report",
"dev": "nodemon",
"dev:bash": "nodemon -x 'npm test ; npm start'",
"postgres:docker": "docker run --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:14-alpine",
"redis:docker": "docker run --rm -p 6379:6379 redis:7-alpine --save '' --appendonly no",
"postgres:docker": "docker run --init -it --rm -p 5432:5432 -e POSTGRES_USER=ci_db_user -e POSTGRES_PASSWORD=ci_db_pass postgres:14-alpine",
"redis:docker": "docker run --init -it --rm -p 6379:6379 redis:7-alpine --save '' --appendonly no",
"start": "ts-node src/index.ts",
"tsc": "tsc -p tsconfig.json",
"lint": "eslint src test",
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/userCounter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function userCounter(req: Request, res: Response, next: NextFunction): vo
method: "post",
url: `${config.userCounterURL}/api/v1/addIP?hashedIP=${getIP(req)}`,
httpAgent
}).catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
}).catch(() => /* instanbul skip next */ Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/routes/getSegmentInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ async function handleGetSegmentInfo(req: Request, res: Response): Promise<DBSegm
// deduplicate with set
UUIDs = [ ...new Set(UUIDs)];
// if more than 10 entries, slice
if (UUIDs.length > 10) UUIDs = UUIDs.slice(0, 10);
if (!Array.isArray(UUIDs) || !UUIDs) {
if (!Array.isArray(UUIDs) || !UUIDs?.length) {
res.status(400).send("UUIDs parameter does not match format requirements.");
return;
}
if (UUIDs.length > 10) UUIDs = UUIDs.slice(0, 10);
const DBSegments = await getSegmentsByUUID(UUIDs);
// all uuids failed lookup
if (!DBSegments?.length) {
Expand Down
4 changes: 2 additions & 2 deletions src/routes/getSkipSegmentsByHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export async function getSkipSegmentsByHash(req: Request, res: Response): Promis
try {
await getEtag("skipSegmentsHash", hashPrefix, service)
.then(etag => res.set("ETag", etag))
.catch(() => null);
.catch(/* istanbul ignore next */ () => null);
const output = Object.entries(segments).map(([videoID, data]) => ({
videoID,
segments: data.segments,
}));
return res.status(output.length === 0 ? 404 : 200).json(output);
} catch(e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(`skip segments by hash error: ${e}`);

return res.status(500).send("Internal server error");
Expand Down
5 changes: 3 additions & 2 deletions src/routes/getTopCategoryUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { db } from "../databases/databases";
import { createMemoryCache } from "../utils/createMemoryCache";
import { config } from "../config";
import { Request, Response } from "express";
import { validateCategories } from "../utils/parseParams";

const MILLISECONDS_IN_MINUTE = 60000;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const getTopCategoryUsersWithCache = createMemoryCache(generateTopCategoryUsersStats, config.getTopUsersCacheTimeMinutes * MILLISECONDS_IN_MINUTE);
/* istanbul ignore next */
const maxRewardTimePerSegmentInSeconds = config.maxRewardTimePerSegmentInSeconds ?? 86400;

interface DBSegment {
Expand Down Expand Up @@ -38,7 +40,6 @@ async function generateTopCategoryUsersStats(sortBy: string, category: string) {
}
}


return {
userNames,
viewCounts,
Expand All @@ -51,7 +52,7 @@ export async function getTopCategoryUsers(req: Request, res: Response): Promise<
const sortType = parseInt(req.query.sortType as string);
const category = req.query.category as string;

if (sortType == undefined || !config.categoryList.includes(category) ) {
if (sortType == undefined || !validateCategories([category]) ) {
//invalid request
return res.sendStatus(400);
}
Expand Down
110 changes: 61 additions & 49 deletions src/routes/getTotalStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { config } from "../config";
import { Request, Response } from "express";
import axios from "axios";
import { Logger } from "../utils/logger";
import { getCWSUsers } from "../utils/getCWSUsers";

// A cache of the number of chrome web store users
let chromeUsersCache = 0;
Expand All @@ -29,30 +30,30 @@ let lastFetch: DBStatsData = {
updateExtensionUsers();

export async function getTotalStats(req: Request, res: Response): Promise<void> {

const row = await getStats(!!req.query.countContributingUsers);
const countContributingUsers = Boolean(req.query?.countContributingUsers == "true");
const row = await getStats(countContributingUsers);
lastFetch = row;

if (row !== undefined) {
const extensionUsers = chromeUsersCache + firefoxUsersCache;

//send this result
res.send({
userCount: row.userCount,
activeUsers: extensionUsers,
apiUsers: Math.max(apiUsersCache, extensionUsers),
viewCount: row.viewCount,
totalSubmissions: row.totalSubmissions,
minutesSaved: row.minutesSaved,
});

// Check if the cache should be updated (every ~14 hours)
const now = Date.now();
if (now - lastUserCountCheck > 5000000) {
lastUserCountCheck = now;

updateExtensionUsers();
}
/* istanbul ignore if */
if (!row) res.sendStatus(500);
const extensionUsers = chromeUsersCache + firefoxUsersCache;

//send this result
res.send({
userCount: row.userCount ?? 0,
activeUsers: extensionUsers,
apiUsers: Math.max(apiUsersCache, extensionUsers),
viewCount: row.viewCount,
totalSubmissions: row.totalSubmissions,
minutesSaved: row.minutesSaved,
});

// Check if the cache should be updated (every ~14 hours)
const now = Date.now();
if (now - lastUserCountCheck > 5000000) {
lastUserCountCheck = now;

updateExtensionUsers();
}
}

Expand All @@ -67,42 +68,53 @@ function getStats(countContributingUsers: boolean): Promise<DBStatsData> {
}
}


function updateExtensionUsers() {
/* istanbul ignore else */
if (config.userCounterURL) {
axios.get(`${config.userCounterURL}/api/v1/userCount`)
.then(res => {
apiUsersCache = Math.max(apiUsersCache, res.data.userCount);
})
.catch(() => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
.then(res => apiUsersCache = Math.max(apiUsersCache, res.data.userCount))
.catch( /* istanbul ignore next */ () => Logger.debug(`Failing to connect to user counter at: ${config.userCounterURL}`));
}

const mozillaAddonsUrl = "https://addons.mozilla.org/api/v3/addons/addon/sponsorblock/";
const chromeExtensionUrl = "https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone";
const chromeExtId = "mnjggcdmjocbbbhaepdhchncahnbgone";

axios.get(mozillaAddonsUrl)
.then(res => {
firefoxUsersCache = res.data.average_daily_users;
axios.get(chromeExtensionUrl)
.then(res => {
const body = res.data;
// 2021-01-05
// [...]<span><meta itemprop="interactionCount" content="UserDownloads:100.000+"/><meta itemprop="opera[...]
const matchingString = '"UserDownloads:';
const matchingStringLen = matchingString.length;
const userDownloadsStartIndex = body.indexOf(matchingString);
if (userDownloadsStartIndex >= 0) {
const closingQuoteIndex = body.indexOf('"', userDownloadsStartIndex + matchingStringLen);
const userDownloadsStr = body.substr(userDownloadsStartIndex + matchingStringLen, closingQuoteIndex - userDownloadsStartIndex).replace(",", "").replace(".", "");
chromeUsersCache = parseInt(userDownloadsStr);
}
else {
lastUserCountCheck = 0;
}
})
.catch(() => Logger.debug(`Failing to connect to ${chromeExtensionUrl}`));
})
.catch(() => {
.then(res => firefoxUsersCache = res.data.average_daily_users )
.catch( /* istanbul ignore next */ () => {
Logger.debug(`Failing to connect to ${mozillaAddonsUrl}`);
return 0;
});
getCWSUsers(chromeExtId)
.then(res => chromeUsersCache = res)
.catch(/* istanbul ignore next */ () =>
getChromeUsers(chromeExtensionUrl)
.then(res => chromeUsersCache = res)
);
}

/* istanbul ignore next */
function getChromeUsers(chromeExtensionUrl: string): Promise<number> {
return axios.get(chromeExtensionUrl)
.then(res => {
const body = res.data;
// 2021-01-05
// [...]<span><meta itemprop="interactionCount" content="UserDownloads:100.000+"/><meta itemprop="opera[...]
const matchingString = '"UserDownloads:';
const matchingStringLen = matchingString.length;
const userDownloadsStartIndex = body.indexOf(matchingString);
/* istanbul ignore else */
if (userDownloadsStartIndex >= 0) {
const closingQuoteIndex = body.indexOf('"', userDownloadsStartIndex + matchingStringLen);
const userDownloadsStr = body.substr(userDownloadsStartIndex + matchingStringLen, closingQuoteIndex - userDownloadsStartIndex).replace(",", "").replace(".", "");
return parseInt(userDownloadsStr);
} else {
lastUserCountCheck = 0;
}
})
.catch(/* istanbul ignore next */ () => {
Logger.debug(`Failing to connect to ${chromeExtensionUrl}`);
return 0;
});
}
13 changes: 8 additions & 5 deletions src/routes/postSkipSegments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async function sendWebhooks(apiVideoDetails: videoDetails, userID: string, video
// false for a pass - it was confusing and lead to this bug - any use of this function in
// the future could have the same problem.
async function autoModerateSubmission(apiVideoDetails: videoDetails,
submission: { videoID: VideoID; userID: UserID; segments: IncomingSegment[], service: Service, videoDuration: number }) {
submission: { videoID: VideoID; userID: HashedUserID; segments: IncomingSegment[], service: Service, videoDuration: number }) {
// get duration from API
const apiDuration = apiVideoDetails.duration;
// if API fail or returns 0, get duration from client
Expand Down Expand Up @@ -156,7 +156,7 @@ async function autoModerateSubmission(apiVideoDetails: videoDetails,
return false;
}

async function checkUserActiveWarning(userID: string): Promise<CheckResult> {
async function checkUserActiveWarning(userID: HashedUserID): Promise<CheckResult> {
const MILLISECONDS_IN_HOUR = 3600000;
const now = Date.now();
const warnings = (await db.prepare("all",
Expand Down Expand Up @@ -337,10 +337,10 @@ async function checkEachSegmentValid(rawIP: IPAddress, paramUserID: UserID, user
return CHECK_PASS;
}

async function checkByAutoModerator(videoID: any, userID: any, segments: Array<any>, service:string, apiVideoDetails: videoDetails, videoDuration: number): Promise<CheckResult> {
async function checkByAutoModerator(videoID: VideoID, userID: HashedUserID, segments: IncomingSegment[], service: Service, apiVideoDetails: videoDetails, videoDuration: number): Promise<CheckResult> {
// Auto moderator check
if (service == Service.YouTube) {
const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { userID, videoID, segments, service, videoDuration });
const autoModerateResult = await autoModerateSubmission(apiVideoDetails, { videoID, userID, segments, service, videoDuration });
if (autoModerateResult) {
return {
pass: false,
Expand Down Expand Up @@ -492,7 +492,10 @@ export async function postSkipSegments(req: Request, res: Response): Promise<Res
let { videoID, userID: paramUserID, service, videoDuration, videoDurationParam, segments, userAgent } = preprocessInput(req);

//hash the userID
const userID = await getHashCache(paramUserID || "");
if (!paramUserID) {
return res.status(400).send("No userID provided");
}
const userID: HashedUserID = await getHashCache(paramUserID);

const invalidCheckResult = await checkInvalidFields(videoID, paramUserID, userID, segments, videoDurationParam, userAgent, service);
if (!invalidCheckResult.pass) {
Expand Down
1 change: 1 addition & 0 deletions src/utils/createMemoryCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export function createMemoryCache(memoryFn: (...args: any[]) => void, cacheTimeMs: number): any {
/* istanbul ignore if */
if (isNaN(cacheTimeMs)) cacheTimeMs = 0;

// holds the promise results
Expand Down
13 changes: 13 additions & 0 deletions src/utils/getCWSUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from "axios";
import { Logger } from "../utils/logger";

export const getCWSUsers = (extID: string): Promise<number | undefined> =>
axios.post(`https://chrome.google.com/webstore/ajax/detail?pv=20210820&id=${extID}`)
.then(res => res.data.split("\n")[2])
.then(data => JSON.parse(data))
.then(data => (data[1][1][0][23]).replaceAll(/,|\+/g,""))
.then(data => parseInt(data))
.catch((err) => {
Logger.error(`Error getting chrome users - ${err}`);
return 0;
});
4 changes: 2 additions & 2 deletions src/utils/getHashCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function getFromRedis<T extends string>(key: HashedValue): Promise<T & Has
Logger.debug(`Got data from redis: ${reply}`);
return reply as T & HashedValue;
}
} catch (err) {
} catch (err) /* istanbul ignore next */ {
Logger.error(err as string);
}
}
Expand All @@ -37,7 +37,7 @@ async function getFromRedis<T extends string>(key: HashedValue): Promise<T & Has
const data = getHash(key, cachedHashTimes);

if (!config.redis?.disableHashCache) {
redis.set(redisKey, data).catch((err) => Logger.error(err));
redis.set(redisKey, data).catch(/* istanbul ignore next */ (err) => Logger.error(err));
}

return data as T & HashedValue;
Expand Down
5 changes: 3 additions & 2 deletions src/utils/innerTubeAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,16 @@ export async function getPlayerData (videoID: string, ignoreCache = false): Prom
}
try {
const data = await getFromITube(videoID)
.catch(err => {
.catch(/* istanbul ignore next */ err => {
Logger.warn(`InnerTube API Error for ${videoID}: ${err}`);
return Promise.reject(err);
});
DiskCache.set(cacheKey, data)
.then(() => Logger.debug(`InnerTube API: video information cache set for: ${videoID}`))
.catch((err: any) => Logger.warn(err));
.catch(/* istanbul ignore next */ (err: any) => Logger.warn(err));
return data;
} catch (err) {
/* istanbul ignore next */
return Promise.reject(err);
}
}
2 changes: 1 addition & 1 deletion src/utils/isUserTempVIP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const isUserTempVIP = async (hashedUserID: HashedUserID, videoID: VideoID
try {
const reply = await redis.get(tempVIPKey(hashedUserID));
return reply && reply == channelID;
} catch (e) {
} catch (e) /* istanbul ignore next */ {
Logger.error(e as string);
return false;
}
Expand Down
3 changes: 3 additions & 0 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Logger {
};

constructor() {
/* istanbul ignore if */
if (config.mode === "development") {
this._settings.INFO = true;
this._settings.DEBUG = true;
Expand Down Expand Up @@ -73,9 +74,11 @@ class Logger {

let color = colors.Bright;
if (level === LogLevel.ERROR) color = colors.FgRed;
/* istanbul ignore if */
if (level === LogLevel.WARN) color = colors.FgYellow;

let levelStr = level.toString();
/* istanbul ignore if */
if (levelStr.length === 4) {
levelStr += " "; // ensure logs are aligned
}
Expand Down
3 changes: 3 additions & 0 deletions src/utils/parseParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ export const parseActionTypes = (req: Request, fallback: ActionType[]): ActionTy

export const parseRequiredSegments = (req: Request): SegmentUUID[] | undefined =>
syntaxErrorWrapper(getRequiredSegments, req, []); // never fall back

export const validateCategories = (categories: string[]): boolean =>
categories.every((category: string) => config.categoryList.includes(category));
4 changes: 4 additions & 0 deletions src/utils/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,21 @@ if (config.redis?.enabled) {
.then((reply) => resolve(reply))
.catch((err) => reject(err))
);
/* istanbul ignore next */
client.on("error", function(error) {
lastClientFail = Date.now();
Logger.error(`Redis Error: ${error}`);
});
/* istanbul ignore next */
client.on("reconnect", () => {
Logger.info("Redis: trying to reconnect");
});
/* istanbul ignore next */
readClient?.on("error", function(error) {
lastReadFail = Date.now();
Logger.error(`Redis Read-Only Error: ${error}`);
});
/* istanbul ignore next */
readClient?.on("reconnect", () => {
Logger.info("Redis Read-Only: trying to reconnect");
});
Expand Down
1 change: 0 additions & 1 deletion test.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
]
}
],
"maxNumberOfActiveWarnings": 3,
"hoursAfterWarningExpires": 24,
"rateLimit": {
"vote": {
Expand Down
Loading

0 comments on commit d76ee7c

Please sign in to comment.