diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index dcae914d..917cae60 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -6,8 +6,7 @@ import { getSubmissionUUID } from "../utils/getSubmissionUUID"; import { getHash } from "../utils/getHash"; import { getHashCache } from "../utils/getHashCache"; import { getIP } from "../utils/getIP"; -import { getFormattedTime } from "../utils/getFormattedTime"; -import { dispatchEvent } from "../utils/webhookUtils"; +import { createDiscordSegmentEmbed, dispatchEvent } from "../utils/webhookUtils"; import { Request, Response } from "express"; import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model"; import { deleteLockCategories } from "./deleteLockCategories"; @@ -24,6 +23,7 @@ import { canSubmit } from "../utils/permissions"; import { getVideoDetails, videoDetails } from "../utils/getVideoDetails"; import * as youtubeID from "../utils/youtubeID"; import { banUser } from "./shadowBanUser"; +import { authorType } from "../types/webhook.model"; type CheckResult = { pass: boolean, @@ -37,67 +37,44 @@ const CHECK_PASS: CheckResult = { errorCode: 0 }; -async function sendWebhookNotification(userID: string, videoID: string, UUID: string, submissionCount: number, youtubeData: videoDetails, { submissionStart, submissionEnd }: { submissionStart: number; submissionEnd: number; }, segmentInfo: any) { - const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]); - const userName = row !== undefined ? row.userName : null; - - let scopeName = "submissions.other"; - if (submissionCount <= 1) { - scopeName = "submissions.new"; - } - - dispatchEvent(scopeName, { - "video": { - "id": videoID, - "title": youtubeData?.title, - "thumbnail": getMaxResThumbnail(videoID), - "url": `https://www.youtube.com/watch?v=${videoID}`, - }, - "submission": { - "UUID": UUID, - "category": segmentInfo.category, - "startTime": submissionStart, - "endTime": submissionEnd, - "user": { - "UUID": userID, - "username": userName, - }, - }, - }); -} - async function sendWebhooks(apiVideoDetails: videoDetails, userID: string, videoID: string, UUID: string, segmentInfo: any, service: Service) { if (apiVideoDetails && service == Service.YouTube) { const userSubmissionCountRow = await db.prepare("get", `SELECT count(*) as "submissionCount" FROM "sponsorTimes" WHERE "userID" = ?`, [userID]); + const row = await db.prepare("get", `SELECT "userName" FROM "userNames" WHERE "userID" = ?`, [userID]); + const username = row?.userName ?? null; const startTime = parseFloat(segmentInfo.segment[0]); const endTime = parseFloat(segmentInfo.segment[1]); - sendWebhookNotification(userID, videoID, UUID, userSubmissionCountRow.submissionCount, apiVideoDetails, { - submissionStart: startTime, - submissionEnd: endTime, - }, segmentInfo).catch((e) => Logger.error(`sending webhooks: ${e}`)); + const newUser = userSubmissionCountRow.submissionCount <= 1; + const webhookData = { + user: { + status: newUser ? authorType.New : authorType.Other + }, + video: { + id: (videoID as VideoID), + title: apiVideoDetails?.title, + url: `https://www.youtube.com/watch?v=${videoID}`, + thumbnail: getMaxResThumbnail(videoID), + }, + submission: { + UUID: UUID as SegmentUUID, + category: segmentInfo.category, + startTime: startTime, + endTime: endTime, + user: { + userID: userID as HashedUserID, + username, + }, + } + }; + const discordEmbed = createDiscordSegmentEmbed(webhookData); + dispatchEvent(newUser ? "submissions.new": "submissions.other", webhookData); // If it is a first time submission // Then send a notification to discord - if (config.discordFirstTimeSubmissionsWebhookURL === null || userSubmissionCountRow.submissionCount > 1) return; - - axios.post(config.discordFirstTimeSubmissionsWebhookURL, { - embeds: [{ - title: apiVideoDetails.title, - url: `https://www.youtube.com/watch?v=${videoID}&t=${(parseInt(startTime.toFixed(0)) - 2)}s#requiredSegment=${UUID}`, - description: `Submission ID: ${UUID}\ - \n\nTimestamp: \ - ${getFormattedTime(startTime)} to ${getFormattedTime(endTime)}\ - \n\nCategory: ${segmentInfo.category}`, - color: 10813440, - author: { - name: userID, - }, - thumbnail: { - url: getMaxResThumbnail(videoID), - }, - }], - }) + if (!config.discordFirstTimeSubmissionsWebhookURL || userSubmissionCountRow.submissionCount > 1) return; + + axios.post(config.discordFirstTimeSubmissionsWebhookURL, { embeds: [discordEmbed] }) .then(res => { if (res.status >= 400) { Logger.error("Error sending first time submission Discord hook"); diff --git a/src/routes/voteOnSponsorTime.ts b/src/routes/voteOnSponsorTime.ts index 5aa748bf..ad87afb3 100644 --- a/src/routes/voteOnSponsorTime.ts +++ b/src/routes/voteOnSponsorTime.ts @@ -4,8 +4,8 @@ import { isUserVIP } from "../utils/isUserVIP"; import { isUserTempVIP } from "../utils/isUserTempVIP"; import { getMaxResThumbnail, YouTubeAPI } from "../utils/youtubeApi"; import { db, privateDB } from "../databases/databases"; -import { dispatchEvent, getVoteAuthor, getVoteAuthorRaw } from "../utils/webhookUtils"; -import { getFormattedTime } from "../utils/getFormattedTime"; +import { dispatchEvent, getVoteAuthorRaw, createDiscordVoteEmbed } from "../utils/webhookUtils"; +import { WebhookData } from "../types/webhook.model"; import { getIP } from "../utils/getIP"; import { getHashCache } from "../utils/getHashCache"; import { config } from "../config"; @@ -35,7 +35,7 @@ interface FinalResponse { } interface VoteData { - UUID: string; + UUID: SegmentUUID; nonAnonUserID: string; originalType: VoteType; voteTypeEnum: number; @@ -47,7 +47,7 @@ interface VoteData { views: number; locked: boolean; }; - category: string; + category: Category; incrementAmount: number; oldIncrementAmount: number; finalResponse: FinalResponse; @@ -101,7 +101,7 @@ async function checkVideoDuration(UUID: SegmentUUID) { } async function sendWebhooks(voteData: VoteData) { - const submissionInfoRow = await db.prepare("get", `SELECT "s"."videoID", "s"."userID", s."startTime", s."endTime", s."category", u."userName", + 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"=?`, @@ -133,62 +133,44 @@ async function sendWebhooks(voteData: VoteData) { 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), + const webhookData: WebhookData = { + 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=${videoID}`, - "thumbnail": getMaxResThumbnail(videoID), + video: { + id: submissionInfoRow.videoID, + title: data?.title, + url: `https://www.youtube.com/watch?v=${videoID}`, + thumbnail: getMaxResThumbnail(videoID), }, - "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, + submission: { + UUID: voteData.UUID as SegmentUUID, + views: voteData.row.views, + locked: voteData.row.locked, + category: voteData.category as Category, + startTime: submissionInfoRow.startTime, + endTime: submissionInfoRow.endTime, + user: { + userID: 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), + votes: { + before: voteData.row.votes, + after: (voteData.row.votes + voteData.incrementAmount - voteData.oldIncrementAmount), }, - }); + authorName: voteData.finalResponse?.webhookMessage ?? voteData.finalResponse?.finalMessage + }; + dispatchEvent(isUpvote ? "vote.up" : "vote.down", webhookData); // 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(videoID), - }, - }], + "embeds": [createDiscordVoteEmbed(webhookData)], }) .then(res => { if (res.status >= 400) { diff --git a/src/types/webhook.model.ts b/src/types/webhook.model.ts new file mode 100644 index 00000000..4b430d83 --- /dev/null +++ b/src/types/webhook.model.ts @@ -0,0 +1,48 @@ +import * as segments from "./segments.model"; +import { HashedUserID } from "./user.model"; + +export enum voteType { + up = "vote.up", + down = "vote.down", +} + +export enum authorType { + Self = "self", + TempVIP = "temp vip", + VIP = "vip", + New = "new", + Other = "other", +} + +export interface WebhookData { + user: { + status: authorType + } + video: { + id: segments.VideoID + title: string | undefined, + url: URL | string, + thumbnail: URL | string, + }, + submission: { + UUID: segments.SegmentUUID, + views?: number, + locked?: boolean, + category: segments.Category, + startTime: number, + endTime: number, + user: { + userID: HashedUserID, + username: string | HashedUserID, + submissions?: { + total: number, + ignored: number, + }, + }, + }, + votes?: { + before: number, + after: number + } + authorName?: string +} \ No newline at end of file diff --git a/src/utils/innerTubeAPI.ts b/src/utils/innerTubeAPI.ts index dbdd728f..ac34fafd 100644 --- a/src/utils/innerTubeAPI.ts +++ b/src/utils/innerTubeAPI.ts @@ -34,7 +34,7 @@ async function getFromITube (videoID: string): Promise { context: { client: { clientName: "WEB", - clientVersion: "2.20221215.04.01" + clientVersion: "2.20230217.01.00" } }, videoId: videoID diff --git a/src/utils/webhookUtils.ts b/src/utils/webhookUtils.ts index 7180a98f..8a3d3fe4 100644 --- a/src/utils/webhookUtils.ts +++ b/src/utils/webhookUtils.ts @@ -1,38 +1,72 @@ import { config } from "../config"; import { Logger } from "../utils/logger"; +import { authorType, WebhookData } from "../types/webhook.model"; +import { getFormattedTime } from "../utils/getFormattedTime"; import axios from "axios"; -function getVoteAuthorRaw(submissionCount: number, isTempVIP: boolean, isVIP: boolean, isOwnSubmission: boolean): string { - if (isOwnSubmission) { - return "self"; - } else if (isTempVIP) { - return "temp vip"; - } else if (isVIP) { - return "vip"; - } else if (submissionCount === 0) { - return "new"; - } else { - return "other"; - } +function getVoteAuthorRaw(submissionCount: number, isTempVIP: boolean, isVIP: boolean, isOwnSubmission: boolean): authorType { + if (isOwnSubmission) return authorType.Self; + else if (isTempVIP) return authorType.TempVIP; + else if (isVIP) return authorType.VIP; + else if (submissionCount === 0) authorType.New; + else return authorType.Other; } -function getVoteAuthor(submissionCount: number, isTempVIP: boolean, isVIP: boolean, isOwnSubmission: boolean): string { - if (isOwnSubmission) { - return "Report by Submitter"; - } else if (isTempVIP) { - return "Report by Temp VIP"; - } else if (isVIP) { - return "Report by VIP User"; - } else if (submissionCount === 0) { - return "Report by New User"; - } +const voteAuthorMap: Record = { + [authorType.Self]: "Report by Submitter", + [authorType.TempVIP]: "Report by Temp VIP", + [authorType.VIP]: "Report by VIP User", + [authorType.New]: "Report by New User", + [authorType.Other]: "" +}; - return ""; -} +const youtubeUrlCreator = (videoId: string, startTime: number, uuid: string): string => { + const startTimeNumber = Math.max(0, startTime - 2); + const startTimeParam = startTime > 0 ? `&t=${startTimeNumber}s` : ""; + return `https://www.youtube.com/watch?v=${videoId}${startTimeParam}#requiredSegment=${uuid}`; +}; -function dispatchEvent(scope: string, data: Record): void { +const createDiscordVoteEmbed = (data: WebhookData) => ({ + title: data.video.title, + url: youtubeUrlCreator(data.video.id, data.submission.startTime, data.submission.UUID), + description: `**${data.votes.before} Votes Prior | \ + ${(data.votes.after)} Votes Now | ${data.submission.views} \ + Views**\n\n**Locked**: ${data.submission.locked}\n\n**Submission ID:** ${data.submission.UUID}\ + \n**Category:** ${data.submission.category}\ + \n\n**Submitted by:** ${data.submission.user.username}\n${data.submission.user.userID}\ + \n\n**Total User Submissions:** ${data.submission.user.submissions.total}\ + \n**Ignored User Submissions:** ${data.submission.user.submissions.ignored}\ + \n\n**Timestamp:** \ + ${getFormattedTime(data.submission.startTime)} to ${getFormattedTime(data.submission.endTime)}`, + color: 10813440, + author: { + name: data.authorName ?? `${voteAuthorMap[data.user.status]}${data.submission.locked ? " (Locked)" : ""}`, + }, + thumbnail: { + url: data.video.thumbnail, + }, +}); + +export const createDiscordSegmentEmbed = (data: WebhookData) => ({ + title: data.video.title, + url: youtubeUrlCreator(data.video.id, data.submission.startTime, data.submission.UUID), + description: `Submission ID: ${data.submission.UUID}\ + \n\nTimestamp: \ + ${getFormattedTime(data.submission.startTime)} to ${getFormattedTime(data.submission.endTime)}\ + \n\nCategory: ${data.submission.category}`, + color: 10813440, + author: { + name: data.submission.user.userID, + }, + thumbnail: { + url: data.video.thumbnail + }, +}); + + +function dispatchEvent(scope: string, data: WebhookData): void { const webhooks = config.webhooks; - if (webhooks === undefined || webhooks.length === 0) return; + if (!webhooks?.length) return; Logger.debug("Dispatching webhooks"); for (const webhook of webhooks) { @@ -59,6 +93,6 @@ function dispatchEvent(scope: string, data: Record): void { export { getVoteAuthorRaw, - getVoteAuthor, dispatchEvent, -}; + createDiscordVoteEmbed +}; \ No newline at end of file diff --git a/test/cases/webhoook.ts b/test/cases/webhoook.ts new file mode 100644 index 00000000..e69de29b