From a2fced0b7334071053aa7df04e3116f8ef77e288 Mon Sep 17 00:00:00 2001 From: Hegedus Gergely Date: Mon, 8 Nov 2021 09:36:17 +0200 Subject: [PATCH] initial commit --- background.js | 26 ++++++ content-styles.css | 3 + content.js | 69 ++++++++++++++++ deleteallcomment.js | 1 + deleting.css | 156 +++++++++++++++++++++++++++++++++++ deleting.html | 43 ++++++++++ deleting.js | 197 ++++++++++++++++++++++++++++++++++++++++++++ hello.html | 11 +++ manifest.json | 23 ++++++ 9 files changed, 529 insertions(+) create mode 100644 background.js create mode 100644 content-styles.css create mode 100644 content.js create mode 100644 deleteallcomment.js create mode 100644 deleting.css create mode 100644 deleting.html create mode 100644 deleting.js create mode 100644 hello.html create mode 100644 manifest.json diff --git a/background.js b/background.js new file mode 100644 index 0000000..63879c6 --- /dev/null +++ b/background.js @@ -0,0 +1,26 @@ +var token = "" +var userId = "" +var videoId = "" + +chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { + if (request.tokenWithUrl != null) { + token = request.tokenWithUrl.replace("https://www.youtube.com/deleteallcomments#access_token=", "").replace(/&.*/g, "") + + let url = chrome.runtime.getURL("deleting.html"); + chrome.tabs.remove(sender.tab.id); + chrome.tabs.create({url}); + } else if (request.toDeleteUserId != null) { + userId = request.toDeleteUserId.replace("https://www.youtube.com/channel/", ""); + } else if (request.videoUrl != null) { + videoId = request.videoUrl.replace("https://www.youtube.com/watch?v=", "").replace(/&.*/g, ""); + } else if (request.closeMe != null) { + chrome.tabs.remove(sender.tab.id); + } else { + sendResponse({ + youtubetoken: token, + toDeleteUserId: userId, + videoId: videoId + }); + } + +}); \ No newline at end of file diff --git a/content-styles.css b/content-styles.css new file mode 100644 index 0000000..acb40db --- /dev/null +++ b/content-styles.css @@ -0,0 +1,3 @@ +.custom-youtube-button-class { + cursor: pointer; +} \ No newline at end of file diff --git a/content.js b/content.js new file mode 100644 index 0000000..97121b6 --- /dev/null +++ b/content.js @@ -0,0 +1,69 @@ +let observer = new MutationObserver((mutations) => { // observe changes on the page, comments are loaded separately so we need this to wait for them + mutations.forEach((mutation) => { + if (!mutation.addedNodes) return + + for (let i = 0; i < mutation.addedNodes.length; i++) { + let node = mutation.addedNodes[i]; + + if (node.id == "reply-dialog") { // we find the reply-dialog place holder + for (let j = 0; j < node.parentElement.childElementCount; j++) { + let possibleToolbar = node.parentElement.children[j]; + if (possibleToolbar.id == "toolbar") { // let's find the heath/reply toolbar + var spanTag = createDeleteAllCommentsCTA(); + possibleToolbar.appendChild(spanTag); + } + } + } + } + }) +}); + +function createDeleteAllCommentsCTA() { + var spanTag = document.createElement("span"); + spanTag.setAttribute( 'class', 'custom-youtube-button-class' ); + spanTag.innerHTML = "DELETE All COMMENTS" + spanTag.style.font = "12px arial,serif"; + spanTag.addEventListener("click", deleteAllButtonClicked); + return spanTag +} + +function deleteAllButtonClicked(pointerEvent) { + searchingCommentParent = findMainParentOfReplyCTA(pointerEvent) + var referenceUrl = findUrlToUser(searchingCommentParent).href; + + chrome.runtime.sendMessage({videoUrl: location.href}); // save video URL + chrome.runtime.sendMessage({toDeleteUserId: referenceUrl}); // save user URL + + // get authentication token + window.open("https://accounts.google.com/o/oauth2/v2/auth?client_id="+google_login_client_id+"&redirect_uri=https://youtube.com/deleteallcomments&response_type=token&scope=https://www.googleapis.com/auth/youtube.force-ssl") +} + +function findMainParentOfReplyCTA(pointerEvent) { + var searchingCommentParent = pointerEvent.currentTarget; + while(searchingCommentParent.id != "main") { + searchingCommentParent = searchingCommentParent.parentElement; + } + return searchingCommentParent; +} + +function findUrlToUser(mainElement) { + for (var i = 0; i < mainElement.childElementCount; i++) { + if (mainElement.id == "author-text") { + return mainElement; + } else { + var childResult = findUrlToUser(mainElement.children[i]); + if (childResult != null) { + return childResult; + } + } + } + return null; +} + + +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + characterData: false +}); \ No newline at end of file diff --git a/deleteallcomment.js b/deleteallcomment.js new file mode 100644 index 0000000..d3f9ade --- /dev/null +++ b/deleteallcomment.js @@ -0,0 +1 @@ +chrome.runtime.sendMessage({tokenWithUrl: location.href}); // just give the data to the background js \ No newline at end of file diff --git a/deleting.css b/deleting.css new file mode 100644 index 0000000..27d427e --- /dev/null +++ b/deleting.css @@ -0,0 +1,156 @@ +/* infinite loading indicator */ +.loader { + margin-top: 30px; + border: 4px solid #CCCCCC; + border-radius: 100%; + border-top: 4px solid #7d3a94; + width: 48px; + height: 48px; + animation: spin 1s infinite cubic-bezier(.1, .4, .7, 0.1); +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* progress bar with percentage value */ +/* expected HTML +
+
+
100%
+
+
+
+
+*/ +.progress-bar { + visibility: hidden; + margin-top: 30px; + width: 56px; + height: 56px; +} + +.progressbar-background { + position: absolute; + border: 4px solid #CCCCCC; + border-radius: 100%; + width: 48px; + height: 48px; +} + +.progressbar-progress { + position: absolute; + border: 4px solid transparent; + border-top: 4px solid #7d3a94; + border-right: 4px solid #7d3a94; + border-radius: 100%; + width: 48px; + height: 48px; + transform: rotate(45deg); +} + +.progressbar-progress-cut { + position: absolute; + border: 4px solid transparent; + border-top: 4px solid #CCCCCC; + border-right: 4px solid #CCCCCC; + border-radius: 100%; + width: 48px; + height: 48px; + transform: rotate(45deg); +} + +.progress-number { + position: absolute; + font: 16px arial, serif; + margin: auto; + text-align: center; + display:flex; + align-items: center; + justify-content: center; + height: 56px; + width: 56px; + color: #CCCCCC +} + +/* general texts */ +span { + font: 20px arial, serif; + color: #CCCCCC; +} + +p { + font: 20px arial, serif; + color: #CCCCCC +} + +a { + font: 20px arial, serif; + color: #DDDDDD +} + +/* additional space for the progress process elements */ +.progress { + margin-top: 30px; +} + +/* sample button */ +.infoButton { + background-color: transparent; + border: 2px solid #7d3a94; + color: #CCCCCC; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + transition-duration: 0.4s; + border-radius: 16px; + visibility: hidden; +} +.infoButton:hover { + background-color: #7d3a94; +} + +/* delete button */ +.actionButton { + background-color: transparent; + border: 2px solid #6d2a84; + color: #CCCCCC; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + transition-duration: 0.4s; + border-radius: 16px; + visibility: hidden; +} +.actionButton:hover { + background-color: #6d2a84; +} + +/* Comments samle */ +.comment { + margin-top: 6px; + border: 3px solid #7d3a94; + padding: 8px; + padding-left: 16px; + padding-right: 16px; + width: fit-content; +} + +.comment-text { + text-align: start; + font: 16px arial, serif; +} + +.comment-name { + text-align: start; + font: 12px arial, serif; +} \ No newline at end of file diff --git a/deleting.html b/deleting.html new file mode 100644 index 0000000..7d88706 --- /dev/null +++ b/deleting.html @@ -0,0 +1,43 @@ + + + + Deleting comments! + + + + + +
+
+ +
+
+

+
+
+
100%
+
+
+
+
+

+ + + +
+ +
+
+
100%
+
+
+
+
+ +

+

+
+
+
+ + \ No newline at end of file diff --git a/deleting.js b/deleting.js new file mode 100644 index 0000000..348602e --- /dev/null +++ b/deleting.js @@ -0,0 +1,197 @@ +// HTML DOOM ELEMENT IDS +var SEARCH_DESCRIPTION_ID = "search_description" +var FIRST_REQUEST_LOADER_ID = "first-request-loading" +var NUMBER_OF_COMMENT_THREADS_LOADED_ID = "number_of_comment_threads" +var NUMBER_OF_COMMENTS_TO_DELETE_ID = "comments_to_be_deleted" +var DELETE_COMMENTS_NUMBER_PROGRESS_ID = "comments_deleted_so_far" +var COULDNT_DELETE_COMMENTS_NUMBER_PROGRESS_ID = "couldnt_delete_comments_so_far" +var COMMENTS_TO_DELETE_PROGRESS_ID = "-comments" +var COMMENTS_DELETING_PROCESS_ID = "-delete" +var SHOW_SAMPLE_CTA_ID = "show_sample_cta" +var DELETE_ALL_CTA_ID = "delete_all_cta" +var COMMENT_SAMPLE_PLACEHOLDER_ID = "comment_sample_placeholder" + +// global references +var youtubeToken = "" +var toDeleteUserId = "" +var videoId = "" +var commentResponses = [] +var commentsToDelete = [] +var isSampleShown = false + +chrome.runtime.sendMessage({greeting: "hello"}, function(response) { + document.getElementById(SEARCH_DESCRIPTION_ID).innerHTML = "Searching through the comments of the following video: video link (" + response.videoUrl + ") by the following user: user profile (" + response.toDeleteUserId + ")" + youtubeToken = response.youtubetoken; + toDeleteUserId = response.toDeleteUserId; + videoId = response.videoId; + + runFindCommentThreadsRequest("", 0) +}); + +function runFindCommentThreadsRequest(pageToken, numberOfCommentsFoundSoFar) { + const request = new XMLHttpRequest(); + const url = 'https://www.googleapis.com/youtube/v3/commentThreads?part=id,replies,snippet&videoId=' + videoId + "&access_token=" + youtubeToken + "&pageToken=" + pageToken; + request.open("GET", url); + + request.onreadystatechange = (e) => { + if (request.readyState === XMLHttpRequest.DONE) { + var status = request.status; + if (status >= 200 && status < 400) { + var responseJson = JSON.parse(request.responseText) + onCommentThreadsReceived(responseJson, numberOfCommentsFoundSoFar) + } else { + console.log("status = " + request.status + " response =" + request.responseText) + onCommentThreadsAccessError() + } + } + } + request.send(); +} + +function onCommentThreadsAccessError() { + document.getElementById(FIRST_REQUEST_LOADER_ID).style.display = "none" + document.getElementById(NUMBER_OF_COMMENT_THREADS_LOADED_ID).innerHTML = "Something went wrong while accessing comment threads of the video, please try again" +} + +function onCommentThreadsReceived(responseJson, numberOfCommentsFoundSoFar) { + commentResponses.push(responseJson) + var newTotalCommentsCount = numberOfCommentsFoundSoFar + responseJson.pageInfo.totalResults + document.getElementById(NUMBER_OF_COMMENT_THREADS_LOADED_ID).innerHTML = "Comment threads accessed: " + newTotalCommentsCount + if (responseJson.nextPageToken == null) { + onCommentThreadsFound(commentResponses, newTotalCommentsCount) + } else { + runFindCommentThreadsRequest(responseJson.nextPageToken, newTotalCommentsCount) + } +} + +function onCommentThreadsFound(commentResponses, totalCountOfThreads) { + document.getElementById(NUMBER_OF_COMMENT_THREADS_LOADED_ID).innerHTML = "Number Of Comment Threads: " + totalCountOfThreads + document.getElementById(FIRST_REQUEST_LOADER_ID).style.display = "none" + var commentThreadProcessed = 0; + var foundCommentsToDelete = [] + setProgressOfProgressBar(COMMENTS_TO_DELETE_PROGRESS_ID, 0) + + for (var i = 0; i < commentResponses.length; i++) { + var comentThreadResponses = commentResponses[i] + for (var j = 0; j < comentThreadResponses.items.length; j++) { + var commentThread = comentThreadResponses.items[j] + foundCommentsToDelete = foundCommentsToDelete.concat(getMatchingCommentIdsFromCommentThread(commentThread)) + commentThreadProcessed = commentThreadProcessed + 1; + setProgressOfProgressBar(COMMENTS_TO_DELETE_PROGRESS_ID, Math.floor(commentThreadProcessed * 100 / totalCountOfThreads)) + } + } + commentsToDelete = foundCommentsToDelete + document.getElementById(NUMBER_OF_COMMENTS_TO_DELETE_ID).innerHTML = "Number of comments to be deleted: " + foundCommentsToDelete.length + document.getElementById(SHOW_SAMPLE_CTA_ID).style.visibility = "visible" + document.getElementById(SHOW_SAMPLE_CTA_ID).addEventListener("click", showSample); + document.getElementById(DELETE_ALL_CTA_ID).style.visibility = "visible" + document.getElementById(DELETE_ALL_CTA_ID).addEventListener("click", deleteAllComments); +} + +function setProgressOfProgressBar(id, progress) { + document.getElementById("progress-bar" + id).style.visibility = 'visible' + if (progress < 50) { + var progressInDegrees = Math.floor(progress * 3.6) + 45; + document.getElementById("progress-second-half" + id).style.visibility = 'hidden' + document.getElementById("progress-moving" + id).style.transform = "rotate(45deg)"; + document.getElementById("progress-cut" + id).style.transform = "rotate(" + progressInDegrees + "deg)"; + } else { + var progressInDegrees = Math.floor((progress - 50) * 3.6) + 225; + document.getElementById("progress-second-half" + id).style.visibility = 'visible' + document.getElementById("progress-moving" + id).style.transform = "rotate(225deg)"; + document.getElementById("progress-cut" + id).style.transform = "rotate(" + progressInDegrees + "deg)"; + } + document.getElementById("progress-number" + id).innerHTML = progress + "%" +} + +function getMatchingCommentIdsFromCommentThread(commentThreadObject) { + var foundComments = [] + var topLevelComment = commentThreadObject.snippet.topLevelComment + if (isCommentMathingAuthor(topLevelComment)) { + foundComments.push(topLevelComment) + } + + if (commentThreadObject.replies != null && commentThreadObject.replies.comments != null) { + for (i = 0; i < commentThreadObject.replies.comments.length; i++) { + var comment = commentThreadObject.replies.comments[i] + if (isCommentMathingAuthor(comment)) { + foundComments.push(comment) + } + } + } + + return foundComments; +} + +function isCommentMathingAuthor(comment) { + return comment.snippet.authorChannelId.value === toDeleteUserId +} + +function showSample() { + if (isSampleShown) return + isSampleShown = true + + for (i = 0; i < commentsToDelete.length && i < 3; i++) { + var commentToShow = commentsToDelete[i]; + var divTag = document.createElement("div"); + divTag.setAttribute("class", "comment") + + var userNameTag = document.createElement("p"); + divTag.appendChild(userNameTag); + userNameTag.setAttribute("class", "comment-name") + userNameTag.innerHTML = "name: " + commentToShow.snippet.authorDisplayName + + var commentTag = document.createElement("p"); + divTag.appendChild(commentTag); + commentTag.setAttribute("class", "comment-text") + commentTag.innerHTML = "comment: " + commentToShow.snippet.textDisplay + + document.getElementById(COMMENT_SAMPLE_PLACEHOLDER_ID).appendChild(divTag); + } +} + +function deleteAllComments() { + setProgressOfProgressBar(COMMENTS_DELETING_PROCESS_ID, 0) + deleteComment(0, 0) +} + +function deleteComment(index, failures) { + if (commentsToDelete.length <= index) { + onAllCommentsDeleted(failures); + return + } + const request = new XMLHttpRequest(); + const url = 'https://www.googleapis.com/youtube/v3/comments?id=' + commentsToDelete[index].id + "&access_token=" + youtubeToken; + request.open("DELETE", url); + + request.onreadystatechange = (e) => { + if (request.readyState === XMLHttpRequest.DONE) { + + setProgressOfProgressBar(COMMENTS_DELETING_PROCESS_ID, Math.floor((index + 1) / commentsToDelete.length * 100)) + var status = request.status; + var failuresAdjusted = failures + if (status >= 200 && status < 400) { + document.getElementById(DELETE_COMMENTS_NUMBER_PROGRESS_ID).innerHTML = "Number of comments deleted so far: " + (index + 1) + } else { + failuresAdjusted = failures + 1 + document.getElementById(COULDNT_DELETE_COMMENTS_NUMBER_PROGRESS_ID).innerHTML = "Number of comments couldn't delete so far: " + failuresAdjusted + } + deleteComment(index + 1, failuresAdjusted) + } + } + request.send(); +} + +function onAllCommentsDeleted(failures) { + if (failures == 0) { + document.getElementById(DELETE_COMMENTS_NUMBER_PROGRESS_ID).innerHTML = "All comments are deleted(" + commentsToDelete.length + "), closing tab in 3 seconds..." + setTimeout(function() { + chrome.runtime.sendMessage({ + closeMe: "true" + }); + }, 3000); + } else { + var successCount = commentsToDelete.length - failures + document.getElementById(DELETE_COMMENTS_NUMBER_PROGRESS_ID).innerHTML = "Couldn't delete all the comments, but did delete: " + successCount + } +} \ No newline at end of file diff --git a/hello.html b/hello.html new file mode 100644 index 0000000..88180f0 --- /dev/null +++ b/hello.html @@ -0,0 +1,11 @@ + + + + Youtube Spam Purger! + + +

Next to comments on videos will show a DELETE All COMMENTS button.

+

Clicking that will navigate you to a new page where you may delete that user's comments from that video.

+

You may need to login to Google for it to work after clicking the button

+ + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..1d30ab4 --- /dev/null +++ b/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "Youtube Spam Purge!", + "version": "1.0", + "manifest_version": 3, + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "hello.html" + }, + "permissions": [ + "tabs" + ], + "content_scripts": [{ + "css": ["content-styles.css"], + "js": ["content.js","clientid.js"], + "matches": ["https://www.youtube.com/watch?v=*"] + }, + { + "js": ["deleteallcomment.js"], + "matches": ["https://www.youtube.com/deleteallcomments"] +}] +} \ No newline at end of file