Add endpoints for rating endpoint (dislikes)

https://github.com/ajayyy/SponsorBlock/issues/1039
This commit is contained in:
Ajay Ramachandran
2021-11-14 23:09:12 -05:00
parent 7590047c6d
commit bc6db0d109
17 changed files with 415 additions and 14 deletions

View File

@@ -0,0 +1,82 @@
import { Request, Response } from "express";
import { db } from "../../databases/databases";
import { RatingType } from "../../types/ratings.model";
import { Service, VideoID, VideoIDHash } from "../../types/segments.model";
import { getService } from "../../utils/getService";
import { hashPrefixTester } from "../../utils/hashPrefixTester";
import { Logger } from "../../utils/logger";
import { QueryCacher } from "../../utils/queryCacher";
import { ratingHashKey } from "../../utils/redisKeys";
interface DBRating {
videoID: VideoID,
hashedVideoID: VideoIDHash,
service: Service,
type: RatingType,
count: number
}
export async function getRating(req: Request, res: Response): Promise<Response> {
let hashPrefix = req.params.prefix as VideoIDHash;
if (!hashPrefix || !hashPrefixTester(hashPrefix)) {
return res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
}
hashPrefix = hashPrefix.toLowerCase() as VideoIDHash;
let types: RatingType[] = [];
try {
types = req.query.types
? JSON.parse(req.query.types as string)
: req.query.type
? Array.isArray(req.query.type)
? req.query.type
: [req.query.type]
: [RatingType.Upvote, RatingType.Downvote];
if (!Array.isArray(types)) {
return res.status(400).send("Categories parameter does not match format requirements.");
}
types = types.map((type) => parseInt(type as unknown as string, 10));
} catch(error) {
return res.status(400).send("Bad parameter: categories (invalid JSON)");
}
const service: Service = getService(req.query.service, req.body.service);
try {
const ratings = (await getRatings(hashPrefix, service))
.filter((rating) => types.includes(rating.type))
.map((rating) => ({
videoID: rating.videoID,
hash: rating.hashedVideoID,
service: rating.service,
type: rating.type,
count: rating.count
}));
if (ratings) {
res.status(200);
} else {
res.status(404);
}
return res.send(ratings ?? []);
} catch (err) {
Logger.error(err as string);
return res.sendStatus(500);
}
}
async function getRatings(hashPrefix: VideoIDHash, service: Service): Promise<DBRating[]> {
const fetchFromDB = () => db
.prepare(
"all",
`SELECT "videoID", "hashedVideoID", "type", "count" FROM "ratings" WHERE "hashedVideoID" LIKE ? AND "service" = ? ORDER BY "hashedVideoID"`,
[`${hashPrefix}%`, service]
) as Promise<DBRating[]>;
if (hashPrefix.length === 4) {
return await QueryCacher.get(fetchFromDB, ratingHashKey(hashPrefix, service));
}
return fetchFromDB();
}

View File

@@ -0,0 +1,62 @@
import { db, privateDB } from "../../databases/databases";
import { getHash } from "../../utils/getHash";
import { Logger } from "../../utils/logger";
import { Request, Response } from "express";
import { HashedUserID, UserID } from "../../types/user.model";
import { HashedIP, IPAddress, VideoID } from "../../types/segments.model";
import { getIP } from "../../utils/getIP";
import { getService } from "../../utils/getService";
import { RatingType, RatingTypes } from "../../types/ratings.model";
import { config } from "../../config";
export async function postRating(req: Request, res: Response): Promise<Response> {
const privateUserID = req.body.userID as UserID;
const videoID = req.body.videoID as VideoID;
const service = getService(req.query.service, req.body.service);
const type = req.body.type as RatingType;
const enabled = req.body.enabled ?? true;
if (privateUserID == undefined || videoID == undefined || service == undefined || type == undefined
|| (typeof privateUserID !== "string") || (typeof videoID !== "string") || (typeof service !== "string")
|| (typeof type !== "number") || (enabled && (typeof enabled !== "boolean")) || !RatingTypes.includes(type)) {
//invalid request
return res.sendStatus(400);
}
const hashedIP: HashedIP = getHash(getIP(req) + config.globalSalt as IPAddress, 1);
const hashedUserID: HashedUserID = getHash(privateUserID);
const hashedVideoID = getHash(videoID, 1);
try {
// Check if this user has voted before
const existingVote = await privateDB.prepare("get", `SELECT count(*) as "count" FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND "type" = ? AND "userID" = ?`, [videoID, service, type, hashedUserID]);
if (existingVote.count > 0 && !enabled) {
// Undo the vote
await db.prepare("run", `UPDATE "ratings" SET "count" = "count" - 1 WHERE "videoID" = ? AND "service" = ? AND type = ?`, [videoID, service, type]);
await privateDB.prepare("run", `DELETE FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND "type" = ? AND "userID" = ?`, [videoID, service, type, hashedUserID]);
return res.sendStatus(200);
} else if (existingVote.count === 0 && enabled) {
// Make sure there hasn't been another vote from this IP
const existingIPVote = (await privateDB.prepare("get", `SELECT count(*) as "count" FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND "type" = ? AND "hashedIP" = ?`, [videoID, service, type, hashedIP]))
.count > 0;
if (!existingIPVote) {
// Check if general rating already exists, if so increase it
const rating = await db.prepare("get", `SELECT count(*) as "count" FROM "ratings" WHERE "videoID" = ? AND "service" = ? AND type = ?`, [videoID, service, type]);
if (rating.count > 0) {
await db.prepare("run", `UPDATE "ratings" SET "count" = "count" + 1 WHERE "videoID" = ? AND "service" = ? AND type = ?`, [videoID, service, type]);
} else {
await db.prepare("run", `INSERT INTO "ratings" ("videoID", "service", "type", "count", "hashedVideoID") VALUES (?, ?, ?, 1, ?)`, [videoID, service, type, hashedVideoID]);
}
// Create entry in privateDB
await privateDB.prepare("run", `INSERT INTO "ratings" ("videoID", "service", "type", "userID", "timeSubmitted", "hashedIP") VALUES (?, ?, ?, ?, ?, ?)`, [videoID, service, type, hashedUserID, Date.now(), hashedIP]);
}
}
return res.sendStatus(200);
} catch (err) {
Logger.error(err as string);
return res.sendStatus(500);
}
}