mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-06 11:36:58 +03:00
536 lines
26 KiB
TypeScript
536 lines
26 KiB
TypeScript
import { Request, Response } from "express";
|
|
import { Logger } from "../utils/logger";
|
|
import { isUserVIP } from "../utils/isUserVIP";
|
|
import { isUserTempVIP } from "../utils/isUserTempVIP";
|
|
import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi";
|
|
import { APIVideoInfo } from "../types/youtubeApi.model";
|
|
import { db, privateDB } from "../databases/databases";
|
|
import { dispatchEvent, getVoteAuthor, getVoteAuthorRaw } from "../utils/webhookUtils";
|
|
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 { DBSegment, Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash, VideoDuration, ActionType, VoteType } from "../types/segments.model";
|
|
import { QueryCacher } from "../utils/queryCacher";
|
|
import axios from "axios";
|
|
|
|
const voteTypes = {
|
|
normal: 0,
|
|
incorrect: 1,
|
|
};
|
|
|
|
enum VoteWebhookType {
|
|
Normal,
|
|
Rejected
|
|
}
|
|
|
|
interface FinalResponse {
|
|
blockVote: boolean,
|
|
finalStatus: number
|
|
finalMessage: string,
|
|
webhookType: VoteWebhookType,
|
|
webhookMessage: string
|
|
}
|
|
|
|
interface VoteData {
|
|
UUID: string;
|
|
nonAnonUserID: string;
|
|
originalType: VoteType;
|
|
voteTypeEnum: number;
|
|
isTempVIP: boolean;
|
|
isVIP: boolean;
|
|
isOwnSubmission: boolean;
|
|
row: {
|
|
votes: number;
|
|
views: number;
|
|
locked: boolean;
|
|
};
|
|
category: string;
|
|
incrementAmount: number;
|
|
oldIncrementAmount: number;
|
|
finalResponse: FinalResponse;
|
|
}
|
|
|
|
function getYouTubeVideoInfo(videoID: VideoID, ignoreCache = false): Promise<APIVideoInfo> {
|
|
return config.newLeafURLs ? YouTubeAPI.listVideos(videoID, ignoreCache) : null;
|
|
}
|
|
|
|
const videoDurationChanged = (segmentDuration: number, APIDuration: number) => (APIDuration > 0 && Math.abs(segmentDuration - APIDuration) > 2);
|
|
|
|
async function updateSegmentVideoDuration(UUID: SegmentUUID) {
|
|
const { videoDuration, videoID, service } = await db.prepare("get", `select "videoDuration", "videoID", "service" from "sponsorTimes" where "UUID" = ?`, [UUID]);
|
|
let apiVideoInfo: APIVideoInfo = null;
|
|
if (service == Service.YouTube) {
|
|
// don't use cache since we have no information about the video length
|
|
apiVideoInfo = await getYouTubeVideoInfo(videoID);
|
|
}
|
|
const apiVideoDuration = apiVideoInfo?.data?.lengthSeconds as VideoDuration;
|
|
if (videoDurationChanged(videoDuration, apiVideoDuration)) {
|
|
Logger.info(`Video duration changed for ${videoID} from ${videoDuration} to ${apiVideoDuration}`);
|
|
await db.prepare("run", `UPDATE "sponsorTimes" SET "videoDuration" = ? WHERE "UUID" = ?`, [apiVideoDuration, UUID]);
|
|
}
|
|
}
|
|
|
|
async function checkVideoDuration(UUID: SegmentUUID) {
|
|
const { videoID, service } = await db.prepare("get", `select "videoID", "service" from "sponsorTimes" where "UUID" = ?`, [UUID]);
|
|
let apiVideoInfo: APIVideoInfo = null;
|
|
if (service == Service.YouTube) {
|
|
// don't use cache since we have no information about the video length
|
|
apiVideoInfo = await getYouTubeVideoInfo(videoID, true);
|
|
}
|
|
const apiVideoDuration = apiVideoInfo?.data?.lengthSeconds as VideoDuration;
|
|
// if no videoDuration return early
|
|
if (isNaN(apiVideoDuration)) return;
|
|
// fetch latest submission
|
|
const latestSubmission = await db.prepare("get", `SELECT "videoDuration", "UUID", "timeSubmitted"
|
|
FROM "sponsorTimes"
|
|
WHERE "videoID" = ? AND "service" = ? AND
|
|
"hidden" = 0 AND "shadowHidden" = 0 AND
|
|
"actionType" != 'full' AND
|
|
"votes" > -2 AND "videoDuration" != 0
|
|
ORDER BY "timeSubmitted" DESC LIMIT 1`,
|
|
[videoID, service]) as {videoDuration: VideoDuration, UUID: SegmentUUID, timeSubmitted: number};
|
|
|
|
if (latestSubmission && videoDurationChanged(latestSubmission.videoDuration, apiVideoDuration)) {
|
|
Logger.info(`Video duration changed for ${videoID} from ${latestSubmission.videoDuration} to ${apiVideoDuration}`);
|
|
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 1
|
|
WHERE "videoID" = ? AND "service" = ? AND "timeSubmitted" <= ?
|
|
AND "hidden" = 0 AND "shadowHidden" = 0 AND
|
|
"actionType" != 'full' AND "votes" > -2`,
|
|
[videoID, service, latestSubmission.timeSubmitted]);
|
|
}
|
|
}
|
|
|
|
async function sendWebhooks(voteData: VoteData) {
|
|
const submissionInfoRow = await db.prepare("get", `SELECT "s"."videoID", "s"."userID", s."startTime", s."endTime", s."category", u."userName",
|
|
(select count(1) from "sponsorTimes" where "userID" = s."userID") count,
|
|
(select count(1) from "sponsorTimes" where "userID" = s."userID" and votes <= -2) disregarded
|
|
FROM "sponsorTimes" s left join "userNames" u on s."userID" = u."userID" where s."UUID"=?`,
|
|
[voteData.UUID]);
|
|
|
|
const userSubmissionCountRow = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [voteData.nonAnonUserID]);
|
|
|
|
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
|
|
let webhookURL: string = null;
|
|
if (voteData.originalType === VoteType.Malicious) {
|
|
webhookURL = config.discordMaliciousReportWebhookURL;
|
|
} else if (voteData.voteTypeEnum === voteTypes.normal) {
|
|
switch (voteData.finalResponse.webhookType) {
|
|
case VoteWebhookType.Normal:
|
|
webhookURL = config.discordReportChannelWebhookURL;
|
|
break;
|
|
case VoteWebhookType.Rejected:
|
|
webhookURL = config.discordFailedReportChannelWebhookURL;
|
|
break;
|
|
}
|
|
} else if (voteData.voteTypeEnum === voteTypes.incorrect) {
|
|
webhookURL = config.discordCompletelyIncorrectReportWebhookURL;
|
|
}
|
|
|
|
if (config.newLeafURLs !== null) {
|
|
const { err, data } = await YouTubeAPI.listVideos(submissionInfoRow.videoID);
|
|
if (err) return;
|
|
|
|
const isUpvote = voteData.incrementAmount > 0;
|
|
// Send custom webhooks
|
|
dispatchEvent(isUpvote ? "vote.up" : "vote.down", {
|
|
"user": {
|
|
"status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isTempVIP, voteData.isVIP, voteData.isOwnSubmission),
|
|
},
|
|
"video": {
|
|
"id": submissionInfoRow.videoID,
|
|
"title": data?.title,
|
|
"url": `https://www.youtube.com/watch?v=${submissionInfoRow.videoID}`,
|
|
"thumbnail": getMaxResThumbnail(data) || null,
|
|
},
|
|
"submission": {
|
|
"UUID": voteData.UUID,
|
|
"views": voteData.row.views,
|
|
"category": voteData.category,
|
|
"startTime": submissionInfoRow.startTime,
|
|
"endTime": submissionInfoRow.endTime,
|
|
"user": {
|
|
"UUID": submissionInfoRow.userID,
|
|
"username": submissionInfoRow.userName,
|
|
"submissions": {
|
|
"total": submissionInfoRow.count,
|
|
"ignored": submissionInfoRow.disregarded,
|
|
},
|
|
},
|
|
},
|
|
"votes": {
|
|
"before": voteData.row.votes,
|
|
"after": (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount),
|
|
},
|
|
});
|
|
|
|
// Send discord message
|
|
if (webhookURL !== null && !isUpvote) {
|
|
axios.post(webhookURL, {
|
|
"embeds": [{
|
|
"title": data?.title,
|
|
"url": `https://www.youtube.com/watch?v=${submissionInfoRow.videoID}&t=${(submissionInfoRow.startTime.toFixed(0) - 2)}s#requiredSegment=${voteData.UUID}`,
|
|
"description": `**${voteData.row.votes} Votes Prior | \
|
|
${(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount)} Votes Now | ${voteData.row.views} \
|
|
Views**\n\n**Locked**: ${voteData.row.locked}\n\n**Submission ID:** ${voteData.UUID}\
|
|
\n**Category:** ${submissionInfoRow.category}\
|
|
\n\n**Submitted by:** ${submissionInfoRow.userName}\n${submissionInfoRow.userID}\
|
|
\n\n**Total User Submissions:** ${submissionInfoRow.count}\
|
|
\n**Ignored User Submissions:** ${submissionInfoRow.disregarded}\
|
|
\n\n**Timestamp:** \
|
|
${getFormattedTime(submissionInfoRow.startTime)} to ${getFormattedTime(submissionInfoRow.endTime)}`,
|
|
"color": 10813440,
|
|
"author": {
|
|
"name": voteData.finalResponse?.webhookMessage ??
|
|
voteData.finalResponse?.finalMessage ??
|
|
`${getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isTempVIP, voteData.isVIP, voteData.isOwnSubmission)}${voteData.row.locked ? " (Locked)" : ""}`,
|
|
},
|
|
"thumbnail": {
|
|
"url": getMaxResThumbnail(data) || "",
|
|
},
|
|
}],
|
|
})
|
|
.then(res => {
|
|
if (res.status >= 400) {
|
|
Logger.error("Error sending reported submission Discord hook");
|
|
Logger.error(JSON.stringify((res.data)));
|
|
Logger.error("\n");
|
|
}
|
|
})
|
|
.catch(err => {
|
|
Logger.error("Failed to send reported submission Discord hook.");
|
|
Logger.error(JSON.stringify(err));
|
|
Logger.error("\n");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, isTempVIP: boolean, isOwnSubmission: boolean, category: Category
|
|
, hashedIP: HashedIP, finalResponse: FinalResponse): Promise<{ status: number, message?: string }> {
|
|
// Check if they've already made a vote
|
|
const usersLastVoteInfo = await privateDB.prepare("get", `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID], { useReplica: true });
|
|
|
|
if (usersLastVoteInfo?.category === category) {
|
|
// Double vote, ignore
|
|
return { status: finalResponse.finalStatus };
|
|
}
|
|
|
|
const segmentInfo = (await db.prepare("get", `SELECT "category", "actionType", "videoID", "hashedVideoID", "service", "userID", "locked" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
|
[UUID], { useReplica: true })) as {category: Category, actionType: ActionType, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID, locked: number};
|
|
|
|
if (!config.categorySupport[category]?.includes(segmentInfo.actionType) || segmentInfo.actionType === ActionType.Full) {
|
|
return { status: 400, message: `Not allowed to change to ${category} when for segment of type ${segmentInfo.actionType}` };
|
|
}
|
|
if (!config.categoryList.includes(category)) {
|
|
return { status: 400, message: "Category doesn't exist." };
|
|
}
|
|
|
|
// Ignore vote if the next category is locked
|
|
const nextCategoryLocked = await db.prepare("get", `SELECT "videoID", "category" FROM "lockCategories" WHERE "videoID" = ? AND "service" = ? AND "category" = ?`, [segmentInfo.videoID, segmentInfo.service, category], { useReplica: true });
|
|
if (nextCategoryLocked && !isVIP) {
|
|
return { status: 200 };
|
|
}
|
|
|
|
// Ignore vote if the segment is locked
|
|
if (!isVIP && segmentInfo.locked === 1) {
|
|
return { status: 200 };
|
|
}
|
|
|
|
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category], { useReplica: true });
|
|
|
|
const timeSubmitted = Date.now();
|
|
|
|
const voteAmount = (isVIP || isTempVIP) ? 500 : 1;
|
|
const ableToVote = finalResponse.finalStatus === 200
|
|
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID], { useReplica: true })) === undefined;
|
|
|
|
if (ableToVote) {
|
|
// Add the vote
|
|
if ((await db.prepare("get", `select count(*) as count from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category])).count > 0) {
|
|
// Update the already existing db entry
|
|
await db.prepare("run", `update "categoryVotes" set "votes" = "votes" + ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, category]);
|
|
} else {
|
|
// Add a db entry
|
|
await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, category, voteAmount]);
|
|
}
|
|
|
|
// Add the info into the private db
|
|
if (usersLastVoteInfo?.votes > 0) {
|
|
// Reverse the previous vote
|
|
await db.prepare("run", `update "categoryVotes" set "votes" = "votes" - ? where "UUID" = ? and "category" = ?`, [voteAmount, UUID, usersLastVoteInfo.category]);
|
|
|
|
await privateDB.prepare("run", `update "categoryVotes" set "category" = ?, "timeSubmitted" = ?, "hashedIP" = ? where "userID" = ? and "UUID" = ?`, [category, timeSubmitted, hashedIP, userID, UUID]);
|
|
} else {
|
|
await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, category, timeSubmitted]);
|
|
}
|
|
|
|
// See if the submissions category is ready to change
|
|
const currentCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, segmentInfo.category], { useReplica: true });
|
|
|
|
const submissionInfo = await db.prepare("get", `SELECT "userID", "timeSubmitted", "votes" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID], { useReplica: true });
|
|
const isSubmissionVIP = submissionInfo && await isUserVIP(submissionInfo.userID);
|
|
const startingVotes = isSubmissionVIP ? 10000 : 1;
|
|
|
|
// Change this value from 1 in the future to make it harder to change categories
|
|
// Done this way without ORs incase the value is zero
|
|
const currentCategoryCount = currentCategoryInfo?.votes ?? startingVotes;
|
|
|
|
// Add submission as vote
|
|
if (!currentCategoryInfo && submissionInfo) {
|
|
await db.prepare("run", `insert into "categoryVotes" ("UUID", "category", "votes") values (?, ?, ?)`, [UUID, segmentInfo.category, currentCategoryCount]);
|
|
await privateDB.prepare("run", `insert into "categoryVotes" ("UUID", "userID", "hashedIP", "category", "timeSubmitted") values (?, ?, ?, ?, ?)`, [UUID, submissionInfo.userID, "unknown", segmentInfo.category, submissionInfo.timeSubmitted]);
|
|
}
|
|
|
|
const nextCategoryCount = (nextCategoryInfo?.votes || 0) + voteAmount;
|
|
|
|
//TODO: In the future, raise this number from zero to make it harder to change categories
|
|
// VIPs change it every time
|
|
if (isVIP || isTempVIP || isOwnSubmission || nextCategoryCount - currentCategoryCount >= Math.max(Math.ceil(submissionInfo?.votes / 2), 2)) {
|
|
// Replace the category
|
|
await db.prepare("run", `update "sponsorTimes" set "category" = ? where "UUID" = ?`, [category, UUID]);
|
|
}
|
|
}
|
|
QueryCacher.clearSegmentCache(segmentInfo);
|
|
return { status: finalResponse.finalStatus };
|
|
}
|
|
|
|
export function getUserID(req: Request): UserID {
|
|
return req.query.userID as UserID;
|
|
}
|
|
|
|
export async function voteOnSponsorTime(req: Request, res: Response): Promise<Response> {
|
|
const UUID = req.query.UUID as SegmentUUID;
|
|
const paramUserID = getUserID(req);
|
|
const type = req.query.type !== undefined ? parseInt(req.query.type as string) : undefined;
|
|
const category = req.query.category as Category;
|
|
const ip = getIP(req);
|
|
|
|
const result = await vote(ip, UUID, paramUserID, type, category);
|
|
|
|
const response = res.status(result.status);
|
|
if (result.message) {
|
|
return response.send(result.message);
|
|
} else if (result.json) {
|
|
return response.json(result.json);
|
|
} else {
|
|
return response.send();
|
|
}
|
|
}
|
|
|
|
export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID, type: number, category?: Category): Promise<{ status: number, message?: string, json?: unknown }> {
|
|
// missing key parameters
|
|
if (!UUID || !paramUserID || !(type !== undefined || category)) {
|
|
return { status: 400 };
|
|
}
|
|
// Ignore this vote, invalid
|
|
if (paramUserID.length < 30 && config.mode !== "test") {
|
|
return { status: 200 };
|
|
}
|
|
|
|
const originalType = type;
|
|
|
|
//hash the userID
|
|
const nonAnonUserID = await getHashCache(paramUserID);
|
|
const userID = await getHashCache(paramUserID + UUID);
|
|
|
|
// To force a non 200, change this early
|
|
const finalResponse: FinalResponse = {
|
|
blockVote: false,
|
|
finalStatus: 200,
|
|
finalMessage: null,
|
|
webhookType: VoteWebhookType.Normal,
|
|
webhookMessage: null
|
|
};
|
|
|
|
//hash the ip 5000 times so no one can get it from the database
|
|
const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress);
|
|
|
|
const segmentInfo: DBSegment = await db.prepare("get", `SELECT * from "sponsorTimes" WHERE "UUID" = ?`, [UUID]);
|
|
// segment doesnt exist
|
|
if (!segmentInfo) {
|
|
return { status: 404 };
|
|
}
|
|
|
|
const isTempVIP = await isUserTempVIP(nonAnonUserID, segmentInfo.videoID);
|
|
const isVIP = await isUserVIP(nonAnonUserID);
|
|
|
|
//check if user voting on own submission
|
|
const isOwnSubmission = nonAnonUserID === segmentInfo.userID;
|
|
|
|
// disallow vote types 10/11
|
|
if (type === 10 || type === 11) {
|
|
return { status: 400 };
|
|
}
|
|
|
|
const MILLISECONDS_IN_HOUR = 3600000;
|
|
const now = Date.now();
|
|
const warnings = (await db.prepare("all", `SELECT "reason" FROM warnings WHERE "userID" = ? AND "issueTime" > ? AND enabled = 1`,
|
|
[nonAnonUserID, Math.floor(now - (config.hoursAfterWarningExpires * MILLISECONDS_IN_HOUR))],
|
|
));
|
|
|
|
if (warnings.length >= config.maxNumberOfActiveWarnings) {
|
|
const warningReason = warnings[0]?.reason;
|
|
return { status: 403, message: "Vote rejected due to a warning from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. " +
|
|
"Could you please send a message in Discord or Matrix so we can further help you?" +
|
|
`${(warningReason.length > 0 ? ` Warning reason: '${warningReason}'` : "")}` };
|
|
}
|
|
|
|
// no type but has category, categoryVote
|
|
if (!type && category) {
|
|
return categoryVote(UUID, nonAnonUserID, isVIP, isTempVIP, isOwnSubmission, category, hashedIP, finalResponse);
|
|
}
|
|
|
|
// If not upvote, or an upvote on a dead segment (for ActionType.Full)
|
|
if (!isVIP && (type != 1 || segmentInfo.votes <= -2)) {
|
|
const isSegmentLocked = segmentInfo.locked;
|
|
const isVideoLocked = async () => !!(await db.prepare("get", `SELECT "category" FROM "lockCategories" WHERE
|
|
"videoID" = ? AND "service" = ? AND "category" = ? AND "actionType" = ?`,
|
|
[segmentInfo.videoID, segmentInfo.service, segmentInfo.category, segmentInfo.actionType], { useReplica: true }));
|
|
if (isSegmentLocked || await isVideoLocked()) {
|
|
finalResponse.blockVote = true;
|
|
finalResponse.webhookType = VoteWebhookType.Rejected;
|
|
finalResponse.webhookMessage = "Vote rejected: A moderator has decided that this segment is correct";
|
|
}
|
|
}
|
|
|
|
// if on downvoted non-full segment and is not VIP/ tempVIP/ submitter
|
|
if (!isNaN(type) && segmentInfo.votes <= -2 && segmentInfo.actionType !== ActionType.Full &&
|
|
!(isVIP || isTempVIP || isOwnSubmission)) {
|
|
if (type == 1) {
|
|
return { status: 403, message: "Not allowed to upvote segment with too many downvotes unless you are VIP." };
|
|
} else if (type == 0) {
|
|
// Already downvoted enough, ignore
|
|
return { status: 200 };
|
|
}
|
|
}
|
|
|
|
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
|
|
|
|
// no restrictions on checkDuration
|
|
// check duration of all submissions on this video
|
|
if (type <= 0) {
|
|
checkVideoDuration(UUID).catch(Logger.error);
|
|
}
|
|
|
|
try {
|
|
// check if vote has already happened
|
|
const votesRow = await privateDB.prepare("get", `SELECT "type" FROM "votes" WHERE "userID" = ? AND "UUID" = ?`, [userID, UUID], { useReplica: true });
|
|
|
|
// -1 for downvote, 1 for upvote. Maybe more depending on reputation in the future
|
|
// oldIncrementAmount will be zero if row is null
|
|
let incrementAmount = 0;
|
|
let oldIncrementAmount = 0;
|
|
|
|
if (type == VoteType.Upvote) {
|
|
//upvote
|
|
incrementAmount = 1;
|
|
} else if (type === VoteType.Downvote || type === VoteType.Malicious) {
|
|
//downvote
|
|
incrementAmount = -1;
|
|
} else if (type == VoteType.Undo) {
|
|
//undo/cancel vote
|
|
incrementAmount = 0;
|
|
} else {
|
|
//unrecongnised type of vote
|
|
return { status: 400 };
|
|
}
|
|
if (votesRow) {
|
|
if (votesRow.type === VoteType.Upvote) {
|
|
oldIncrementAmount = 1;
|
|
} else if (votesRow.type === VoteType.Downvote) {
|
|
oldIncrementAmount = -1;
|
|
} else if (votesRow.type === VoteType.ExtraDownvote) {
|
|
oldIncrementAmount = -4;
|
|
} else if (votesRow.type === VoteType.Undo) {
|
|
oldIncrementAmount = 0;
|
|
} else if (votesRow.type < 0) {
|
|
//vip downvote
|
|
oldIncrementAmount = votesRow.type;
|
|
} else if (votesRow.type === 12) {
|
|
// VIP downvote for completely incorrect
|
|
oldIncrementAmount = -500;
|
|
} else if (votesRow.type === 13) {
|
|
// VIP upvote for completely incorrect
|
|
oldIncrementAmount = 500;
|
|
}
|
|
}
|
|
|
|
// check if the increment amount should be multiplied (downvotes have more power if there have been many views)
|
|
// user is temp/ VIP/ own submission and downvoting
|
|
if ((isVIP || isTempVIP || isOwnSubmission) && incrementAmount < 0) {
|
|
incrementAmount = -(segmentInfo.votes + 2 - oldIncrementAmount);
|
|
type = incrementAmount;
|
|
}
|
|
|
|
if (type === VoteType.Malicious) {
|
|
incrementAmount = -Math.min(segmentInfo.votes + 2 - oldIncrementAmount, 5);
|
|
type = incrementAmount;
|
|
}
|
|
|
|
// Only change the database if they have made a submission before and haven't voted recently
|
|
const userAbleToVote = (!(isOwnSubmission && incrementAmount > 0 && oldIncrementAmount >= 0)
|
|
&& !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter)
|
|
&& !finalResponse.blockVote
|
|
&& finalResponse.finalStatus === 200
|
|
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) !== undefined
|
|
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) === undefined
|
|
&& (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID], { useReplica: true })) === undefined);
|
|
|
|
|
|
const ableToVote = isVIP || isTempVIP || userAbleToVote;
|
|
|
|
if (ableToVote) {
|
|
//update the votes table
|
|
if (votesRow) {
|
|
await privateDB.prepare("run", `UPDATE "votes" SET "type" = ?, "originalType" = ? WHERE "userID" = ? AND "UUID" = ?`, [type, originalType, userID, UUID]);
|
|
} else {
|
|
await privateDB.prepare("run", `INSERT INTO "votes" ("UUID", "userID", "hashedIP", "type", "normalUserID", "originalType") VALUES(?, ?, ?, ?, ?, ?)`, [UUID, userID, hashedIP, type, nonAnonUserID, originalType]);
|
|
}
|
|
|
|
// update the vote count on this sponsorTime
|
|
await db.prepare("run", `UPDATE "sponsorTimes" SET "votes" = "votes" + ? WHERE "UUID" = ?`, [incrementAmount - oldIncrementAmount, UUID]);
|
|
|
|
// tempVIP can bring back hidden segments
|
|
if (isTempVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
|
|
await db.prepare("run", `UPDATE "sponsorTimes" SET "hidden" = 0 WHERE "UUID" = ?`, [UUID]);
|
|
}
|
|
// additional processing for VIP
|
|
// on VIP upvote
|
|
if (isVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
|
|
// Update video duration in case that caused it to be hidden
|
|
await updateSegmentVideoDuration(UUID);
|
|
// unhide & unlock
|
|
await db.prepare("run", 'UPDATE "sponsorTimes" SET "locked" = 1, "hidden" = 0, "shadowHidden" = 0 WHERE "UUID" = ?', [UUID]);
|
|
// on VIP downvote/ undovote, also unlock submission
|
|
} else if (isVIP && incrementAmount <= 0 && voteTypeEnum === voteTypes.normal) {
|
|
await db.prepare("run", 'UPDATE "sponsorTimes" SET "locked" = 0 WHERE "UUID" = ?', [UUID]);
|
|
}
|
|
|
|
QueryCacher.clearSegmentCache(segmentInfo);
|
|
}
|
|
if (incrementAmount - oldIncrementAmount !== 0) {
|
|
sendWebhooks({
|
|
UUID,
|
|
nonAnonUserID,
|
|
originalType,
|
|
voteTypeEnum,
|
|
isTempVIP,
|
|
isVIP,
|
|
isOwnSubmission,
|
|
row: segmentInfo,
|
|
category,
|
|
incrementAmount,
|
|
oldIncrementAmount,
|
|
finalResponse
|
|
}).catch(Logger.error);
|
|
}
|
|
return { status: finalResponse.finalStatus, message: finalResponse.finalMessage ?? undefined };
|
|
} catch (err) {
|
|
Logger.error(err as string);
|
|
return { status: 500, message: finalResponse.finalMessage ?? undefined, json: { error: "Internal error creating segment vote" } };
|
|
}
|
|
}
|