mirror of
https://github.com/ajayyy/SponsorBlockServer.git
synced 2025-12-14 15:37:07 +03:00
Merge remote-tracking branch 'upstream/master' into fix/prepare-statements
This commit is contained in:
@@ -2,27 +2,34 @@ import {Request, Response} from 'express';
|
||||
import {Logger} from '../utils/logger';
|
||||
import {isUserVIP} from '../utils/isUserVIP';
|
||||
import fetch from 'node-fetch';
|
||||
import {YouTubeAPI} from '../utils/youtubeApi';
|
||||
import {getMaxResThumbnail, YouTubeAPI} from '../utils/youtubeApi';
|
||||
import {db, privateDB} from '../databases/databases';
|
||||
import {dispatchEvent, getVoteAuthor, getVoteAuthorRaw} from '../utils/webhookUtils';
|
||||
import {isUserTrustworthy} from '../utils/isUserTrustworthy';
|
||||
import {getFormattedTime} from '../utils/getFormattedTime';
|
||||
import {getIP} from '../utils/getIP';
|
||||
import {getHash} from '../utils/getHash';
|
||||
import {config} from '../config';
|
||||
import { UserID } from '../types/user.model';
|
||||
import redis from '../utils/redis';
|
||||
import { skipSegmentsHashKey, skipSegmentsKey } from '../middleware/redisKeys';
|
||||
import { Category, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from '../types/segments.model';
|
||||
import { Category, CategoryActionType, HashedIP, IPAddress, SegmentUUID, Service, VideoID, VideoIDHash } from '../types/segments.model';
|
||||
import { getCategoryActionType } from '../utils/categoryInfo';
|
||||
import { QueryCacher } from '../utils/queryCacher';
|
||||
|
||||
const voteTypes = {
|
||||
normal: 0,
|
||||
incorrect: 1,
|
||||
};
|
||||
|
||||
enum VoteWebhookType {
|
||||
Normal,
|
||||
Rejected
|
||||
}
|
||||
|
||||
interface FinalResponse {
|
||||
blockVote: boolean,
|
||||
finalStatus: number
|
||||
finalMessage: string
|
||||
finalMessage: string,
|
||||
webhookType: VoteWebhookType,
|
||||
webhookMessage: string
|
||||
}
|
||||
|
||||
interface VoteData {
|
||||
@@ -53,96 +60,102 @@ async function sendWebhooks(voteData: VoteData) {
|
||||
if (submissionInfoRow !== undefined && userSubmissionCountRow != undefined) {
|
||||
let webhookURL: string = null;
|
||||
if (voteData.voteTypeEnum === voteTypes.normal) {
|
||||
webhookURL = config.discordReportChannelWebhookURL;
|
||||
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.youtubeAPIKey !== null) {
|
||||
YouTubeAPI.listVideos(submissionInfoRow.videoID, (err, data) => {
|
||||
if (err || data.items.length === 0) {
|
||||
err && Logger.error(err.toString());
|
||||
return;
|
||||
}
|
||||
const isUpvote = voteData.incrementAmount > 0;
|
||||
// Send custom webhooks
|
||||
dispatchEvent(isUpvote ? "vote.up" : "vote.down", {
|
||||
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.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": {
|
||||
"status": getVoteAuthorRaw(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission),
|
||||
},
|
||||
"video": {
|
||||
"id": submissionInfoRow.videoID,
|
||||
"title": data.items[0].snippet.title,
|
||||
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID,
|
||||
"thumbnail": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
|
||||
},
|
||||
"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,
|
||||
},
|
||||
"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) {
|
||||
fetch(webhookURL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"embeds": [{
|
||||
"title": data.items[0].snippet.title,
|
||||
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID
|
||||
+ "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2),
|
||||
"description": "**" + voteData.row.votes + " Votes Prior | " +
|
||||
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
|
||||
+ " Views**\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?.finalMessage ?? getVoteAuthor(userSubmissionCountRow.submissionCount, voteData.isVIP, voteData.isOwnSubmission),
|
||||
},
|
||||
"thumbnail": {
|
||||
"url": data.items[0].snippet.thumbnails.maxres ? data.items[0].snippet.thumbnails.maxres.url : "",
|
||||
},
|
||||
}],
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending reported submission Discord hook");
|
||||
Logger.error(JSON.stringify((await res.text())));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send reported submission Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
"votes": {
|
||||
"before": voteData.row.votes,
|
||||
"after": (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount),
|
||||
},
|
||||
});
|
||||
|
||||
// Send discord message
|
||||
if (webhookURL !== null && !isUpvote) {
|
||||
fetch(webhookURL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"embeds": [{
|
||||
"title": data?.title,
|
||||
"url": "https://www.youtube.com/watch?v=" + submissionInfoRow.videoID
|
||||
+ "&t=" + (submissionInfoRow.startTime.toFixed(0) - 2),
|
||||
"description": "**" + voteData.row.votes + " Votes Prior | " +
|
||||
(voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount) + " Votes Now | " + voteData.row.views
|
||||
+ " Views**\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.isVIP, voteData.isOwnSubmission),
|
||||
},
|
||||
"thumbnail": {
|
||||
"url": getMaxResThumbnail(data) || "",
|
||||
},
|
||||
}],
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status >= 400) {
|
||||
Logger.error("Error sending reported submission Discord hook");
|
||||
Logger.error(JSON.stringify((await res.text())));
|
||||
Logger.error("\n");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error("Failed to send reported submission Discord hook.");
|
||||
Logger.error(JSON.stringify(err));
|
||||
Logger.error("\n");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,8 +171,8 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
return;
|
||||
}
|
||||
|
||||
const videoInfo = (await db.prepare('get', `SELECT "category", "videoID", "hashedVideoID", "service" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
||||
[UUID])) as {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service};
|
||||
const videoInfo = (await db.prepare('get', `SELECT "category", "videoID", "hashedVideoID", "service", "userID" FROM "sponsorTimes" WHERE "UUID" = ?`,
|
||||
[UUID])) as {category: Category, videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, userID: UserID};
|
||||
if (!videoInfo) {
|
||||
// Submission doesn't exist
|
||||
res.status(400).send("Submission doesn't exist.");
|
||||
@@ -170,6 +183,10 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
res.status(400).send("Category doesn't exist.");
|
||||
return;
|
||||
}
|
||||
if (getCategoryActionType(category) !== CategoryActionType.Skippable) {
|
||||
res.status(400).send("Cannot vote for this category");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCategoryInfo = await db.prepare("get", `select votes from "categoryVotes" where "UUID" = ? and category = ?`, [UUID, category]);
|
||||
|
||||
@@ -226,7 +243,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i
|
||||
}
|
||||
}
|
||||
|
||||
clearRedisCache(videoInfo);
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
|
||||
res.sendStatus(finalResponse.finalStatus);
|
||||
}
|
||||
@@ -253,8 +270,11 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
|
||||
// To force a non 200, change this early
|
||||
let finalResponse: FinalResponse = {
|
||||
blockVote: false,
|
||||
finalStatus: 200,
|
||||
finalMessage: null
|
||||
finalMessage: null,
|
||||
webhookType: VoteWebhookType.Normal,
|
||||
webhookMessage: null
|
||||
}
|
||||
|
||||
//x-forwarded-for if this server is behind a proxy
|
||||
@@ -277,8 +297,9 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
' where "UUID" = ?', [UUID]));
|
||||
|
||||
if (await isSegmentLocked() || await isVideoLocked()) {
|
||||
finalResponse.finalStatus = 403;
|
||||
finalResponse.finalMessage = "Vote rejected: A moderator has decided that this segment is correct"
|
||||
finalResponse.blockVote = true;
|
||||
finalResponse.webhookType = VoteWebhookType.Rejected
|
||||
finalResponse.webhookMessage = "Vote rejected: A moderator has decided that this segment is correct"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +333,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
return res.status(403).send('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?');
|
||||
}
|
||||
|
||||
const voteTypeEnum = (type == 0 || type == 1) ? voteTypes.normal : voteTypes.incorrect;
|
||||
const voteTypeEnum = (type == 0 || type == 1 || type == 20) ? voteTypes.normal : voteTypes.incorrect;
|
||||
|
||||
try {
|
||||
//check if vote has already happened
|
||||
@@ -362,8 +383,8 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
//check if the increment amount should be multiplied (downvotes have more power if there have been many views)
|
||||
const videoInfo = await db.prepare('get', `SELECT "videoID", "hashedVideoID", "service", "votes", "views" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
|
||||
{videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, votes: number, views: number};
|
||||
const videoInfo = await db.prepare('get', `SELECT "videoID", "hashedVideoID", "service", "votes", "views", "userID" FROM "sponsorTimes" WHERE "UUID" = ?`, [UUID]) as
|
||||
{videoID: VideoID, hashedVideoID: VideoIDHash, service: Service, votes: number, views: number, userID: UserID};
|
||||
|
||||
if (voteTypeEnum === voteTypes.normal) {
|
||||
if ((isVIP || isOwnSubmission) && incrementAmount < 0) {
|
||||
@@ -381,9 +402,11 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
|
||||
// Only change the database if they have made a submission before and haven't voted recently
|
||||
const ableToVote = isVIP
|
||||
|| ((await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined
|
||||
&& (await privateDB.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID])) === undefined
|
||||
|| (!(isOwnSubmission && incrementAmount > 0)
|
||||
&& (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ?`, [nonAnonUserID])) !== undefined
|
||||
&& (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID])) === undefined
|
||||
&& (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID])) === undefined)
|
||||
&& !finalResponse.blockVote
|
||||
&& finalResponse.finalStatus === 200;
|
||||
|
||||
if (ableToVote) {
|
||||
@@ -403,7 +426,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
|
||||
//update the vote count on this sponsorTime
|
||||
//oldIncrementAmount will be zero is row is null
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET ' + columnName + ' = ' + columnName + ' + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]);
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET "' + columnName + '" = "' + columnName + '" + ? WHERE "UUID" = ?', [incrementAmount - oldIncrementAmount, UUID]);
|
||||
if (isVIP && incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
|
||||
// Lock this submission
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 1 WHERE "UUID" = ?', [UUID]);
|
||||
@@ -412,32 +435,7 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET locked = 0 WHERE "UUID" = ?', [UUID]);
|
||||
}
|
||||
|
||||
clearRedisCache(videoInfo);
|
||||
|
||||
//for each positive vote, see if a hidden submission can be shown again
|
||||
if (incrementAmount > 0 && voteTypeEnum === voteTypes.normal) {
|
||||
//find the UUID that submitted the submission that was voted on
|
||||
const submissionUserIDInfo = await db.prepare('get', 'SELECT "userID" FROM "sponsorTimes" WHERE "UUID" = ?', [UUID]);
|
||||
if (!submissionUserIDInfo) {
|
||||
// They are voting on a non-existent submission
|
||||
res.status(400).send("Voting on a non-existent submission");
|
||||
return;
|
||||
}
|
||||
|
||||
const submissionUserID = submissionUserIDInfo.userID;
|
||||
|
||||
//check if any submissions are hidden
|
||||
const hiddenSubmissionsRow = await db.prepare('get', 'SELECT count(*) as "hiddenSubmissions" FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" > 0', [submissionUserID]);
|
||||
|
||||
if (hiddenSubmissionsRow.hiddenSubmissions > 0) {
|
||||
//see if some of this users submissions should be visible again
|
||||
|
||||
if (await isUserTrustworthy(submissionUserID)) {
|
||||
//they are trustworthy again, show 2 of their submissions again, if there are two to show
|
||||
await db.prepare('run', 'UPDATE "sponsorTimes" SET "shadowHidden" = 0 WHERE ROWID IN (SELECT ROWID FROM "sponsorTimes" WHERE "userID" = ? AND "shadowHidden" = 1 LIMIT 2)', [submissionUserID]);
|
||||
}
|
||||
}
|
||||
}
|
||||
QueryCacher.clearVideoCache(videoInfo);
|
||||
}
|
||||
|
||||
res.status(finalResponse.finalStatus).send(finalResponse.finalMessage ?? undefined);
|
||||
@@ -461,11 +459,4 @@ export async function voteOnSponsorTime(req: Request, res: Response) {
|
||||
|
||||
res.status(500).json({error: 'Internal error creating segment vote'});
|
||||
}
|
||||
}
|
||||
|
||||
function clearRedisCache(videoInfo: { videoID: VideoID; hashedVideoID: VideoIDHash; service: Service; }) {
|
||||
if (videoInfo) {
|
||||
redis.delAsync(skipSegmentsKey(videoInfo.videoID));
|
||||
redis.delAsync(skipSegmentsHashKey(videoInfo.hashedVideoID, videoInfo.service));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user