├── data └── whitelist.txt ├── icon.png ├── README.md ├── manifest.json ├── LICENSE ├── Licenses └── LICENSE.txt ├── popup.html ├── background.js ├── popup.js ├── delete_tweet.js └── contentScript.js /data/whitelist.txt: -------------------------------------------------------------------------------- 1 | @FF_XIV_JP 2 | @FF_XIV_EN -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/queeeeeeee/TwitterUtility/HEAD/icon.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitter Utility 2 | 이런저런 기능이 있는 트위터 확장 프로그램입니다. 3 | 4 | 이 프로그램을 사용하는 과정에서 발생할 수 있는 계정 문제에 대해 개발자는 어떠한 책임도 지지 않습니다. 5 | 사용자는 자신의 판단에 따라 이 프로그램을 실행하며, 모든 결과는 사용자 본인의 책임입니다. 6 | 7 | - 트위터 로고를 래리로 바꿉니다 8 | - 인용 보기 버튼을 만들어 줍니다 9 | - 알티 추첨 버튼을 만들어 줍니다 10 | 11 | 1.1 12 | - 트윗 청소기가 추가되었습니다. 13 | 14 | 1.5 15 | - '이상한 것들 지우기'가 추가되었습니다. 16 | 17 | 1.6 18 | - '파란 딱지 숨기기'가 추가되었습니다. 19 | 20 | 1.7 21 | - 트윗 청소기가 개편되었습니다. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Twitter Utility.", 3 | "version": "1.8.2", 4 | "web_accessible_resources": [ 5 | { 6 | "resources": ["data/*"], 7 | "matches": [""] 8 | } 9 | ], 10 | "description": "Twitter Utility.", 11 | "manifest_version": 3, 12 | "background": { 13 | "service_worker": "background.js" 14 | }, 15 | "content_scripts": [{ 16 | "matches": ["https://*.twitter.com/*","https://*.x.com/*"], 17 | "js": [ 18 | "delete_tweet.js", 19 | "contentScript.js" 20 | ] 21 | }], 22 | "action": { 23 | "default_icon": "icon.png", 24 | "default_title": "Twitter Utility", 25 | "default_popup": "popup.html" 26 | }, 27 | "permissions": ["tabs","storage","webRequest"], 28 | "host_permissions": [ 29 | "*://*/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 queeeeeeee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Licenses/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Lyfhael/DeleteTweets 2 | https://github.com/Lyfhael/DeleteTweets 3 | 4 | MIT License 5 | 6 | Copyright (c) 2023 Lyfhael 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | 75 | 76 | 77 | 78 | Twitter Utility 79 | 80 | 86 | 87 | 88 |

Twitter Utility

89 |
90 | 기본 옵션 91 |
92 | 93 | 94 |
95 | 96 |
97 | 98 | 99 |
100 | 101 |
102 | 103 | 104 |
105 |
106 | 107 | 111 |
112 |
113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | async function getActiveTabURL() { 2 | const tabs = await chrome.tabs.query({ 3 | currentWindow: true, 4 | active: true 5 | }); 6 | 7 | return tabs[0]; 8 | } 9 | const OnResult = (result) => {}; 10 | 11 | 12 | async function OnChangeLogo() { 13 | const activeTab = await getActiveTabURL(); 14 | chrome.storage.sync.get(["hideElements"], function(items){ 15 | var message = { 16 | type: "changeLogo", 17 | hideElements: items["hideElements"] 18 | } 19 | 20 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 21 | }); 22 | } 23 | 24 | async function OnRemoveBlueMark(){ 25 | const activeTab = await getActiveTabURL(); 26 | chrome.storage.sync.get(["hideBlueMark","hideBlueMarkButton"], function(items){ 27 | var message = { 28 | type: "hideBlueMark", 29 | hideBlueMark: items["hideBlueMark"], 30 | hideBlueMarkButton: items["hideBlueMarkButton"] 31 | } 32 | 33 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 34 | }); 35 | } 36 | 37 | async function OnMakeButton() { 38 | const activeTab = await getActiveTabURL(); 39 | var message = "makeButton" 40 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 41 | } 42 | 43 | async function OnMakeRandomButton() { 44 | const activeTab = await getActiveTabURL(); 45 | var message = "makeRandomButton" 46 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 47 | } 48 | 49 | function checkIsTwitter(tab) { 50 | return (tab.url.indexOf("twitter.com") != -1 || tab.url.indexOf("x.com") != -1) 51 | } 52 | 53 | function urlContains(tab, text) { 54 | return tab.url.indexOf(text) != -1; 55 | } 56 | 57 | function urlEndsWith(tab, text) { 58 | return tab.url.endsWith(text); 59 | } 60 | 61 | 62 | function extractQueryIdFromUrl(url) { 63 | const match = url.match(/graphql\/([^/]+)/); 64 | return match ? match[1] : null; 65 | } 66 | 67 | function extractFeaturesFromUrl(url) { 68 | const match = url.match(/features=([^&]+)/); 69 | return match ? match[1] : null; 70 | } 71 | 72 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 73 | if (changeInfo.status != "complete") 74 | return; 75 | if (!checkIsTwitter(tab)) 76 | return; 77 | 78 | OnChangeLogo(); 79 | OnRemoveBlueMark(); 80 | 81 | if (urlContains(tab, "status")) { 82 | if (urlEndsWith(tab, "retweets") | urlEndsWith(tab, "quotes") | urlEndsWith(tab, "likes")) { 83 | OnMakeRandomButton(); 84 | } else { 85 | OnMakeButton(); 86 | } 87 | } 88 | }) 89 | 90 | chrome.webRequest.onBeforeRequest.addListener( 91 | function (details) { 92 | if (details.method === "POST") { 93 | // console.log("Captured POST request:", details); 94 | } 95 | }, 96 | { urls: [""] }, 97 | ["requestBody"] 98 | ); 99 | 100 | let savedHeaders = { 101 | authorization: null, 102 | clientTid: null, 103 | clientUuid: null, 104 | platform: null, 105 | id: null, 106 | features: null 107 | }; 108 | 109 | chrome.webRequest.onBeforeSendHeaders.addListener( 110 | function (details) { 111 | if (details.url.includes("UserTweetsAndReplies") && details.url.includes("x.com")) { 112 | const headers = details.requestHeaders; 113 | savedHeaders.id = extractQueryIdFromUrl(details.url) 114 | savedHeaders.features = extractFeaturesFromUrl(details.url); 115 | headers.forEach(header => { 116 | switch (header.name.toLowerCase()) { 117 | case 'authorization': 118 | savedHeaders.authorization = header.value; 119 | break; 120 | case 'x-client-transaction-id': 121 | savedHeaders.clientTid = header.value; 122 | break; 123 | case 'x-client-uuid': 124 | savedHeaders.clientUuid = header.value; 125 | break; 126 | case 'sec-ch-ua-platform': 127 | savedHeaders.platform = header.value 128 | break; 129 | } 130 | }); 131 | } 132 | return { requestHeaders: details.requestHeaders }; 133 | }, 134 | { urls: ["*://*.twitter.com/*", "*://*.x.com/*"] }, 135 | ["requestHeaders"] 136 | ); 137 | 138 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 139 | if (message.type === "getHeaders") { 140 | sendResponse({ 141 | headers: savedHeaders 142 | }); 143 | } 144 | }); -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | function checkIsTwitter(tab) { 2 | return (tab.url.indexOf("twitter.com") != -1 || tab.url.indexOf("x.com") != -1) 3 | } 4 | 5 | async function getActiveTabURL() { 6 | const tabs = await chrome.tabs.query({ 7 | currentWindow: true, 8 | active: true 9 | }); 10 | 11 | return tabs[0]; 12 | } 13 | const OnResult = (result) => {}; 14 | 15 | 16 | async function OnChangeLogo() { 17 | var activeTab = await getActiveTabURL(); 18 | chrome.storage.sync.get(["hideElements"], function(items){ 19 | var message = { 20 | type: "changeLogo", 21 | hideElements: items["hideElements"] 22 | } 23 | 24 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 25 | }); 26 | 27 | activeTab = await getActiveTabURL(); 28 | chrome.storage.sync.get(["hideBlueMark","hideBlueMarkButton"], function(items){ 29 | var message = { 30 | type: "hideBlueMark", 31 | hideBlueMark: items["hideBlueMark"], 32 | hideBlueMarkButton: items["hideBlueMarkButton"] 33 | } 34 | 35 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 36 | }); 37 | } 38 | 39 | function refreshCurrentTab() { 40 | chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { 41 | var tab = tabs[0]; 42 | 43 | console.log('reload start') 44 | chrome.tabs.reload(tab.id,{ bypassCache: true }); 45 | console.log('reload fin') 46 | }); 47 | } 48 | 49 | 50 | const OnHeartClean = async() => { 51 | const activeTab = await getActiveTabURL(); 52 | var slider = document.getElementById("DelayRange"); 53 | var message = { 54 | delay: slider.value, 55 | deleteRetweet: false, 56 | deleteMytweet: false, 57 | isDeleteHeart: true 58 | } 59 | 60 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 61 | } 62 | 63 | const OnTweetClean = async() => { 64 | var value1 = document.getElementById("checkbox1").checked; 65 | var value2 = document.getElementById("checkbox2").checked; 66 | var keywords = document.getElementById("excludeKeywords").value 67 | .split(',') 68 | .map(k => k.trim()) 69 | .filter(k => k.length > 0); 70 | var startDate = document.getElementById("startDate").value; 71 | var endDate = document.getElementById("endDate").value; 72 | 73 | const activeTab = await getActiveTabURL(); 74 | var message = { 75 | deleteRetweet: value1, 76 | deleteMytweet: value2, 77 | isDeleteHeart: false, 78 | excludeKeywords: keywords, 79 | startDate: startDate ? new Date(startDate) : new Date(0), 80 | endDate: endDate ? new Date(endDate) : new Date() 81 | } 82 | 83 | chrome.tabs.sendMessage(activeTab.id, message, OnResult); 84 | } 85 | 86 | document.addEventListener("DOMContentLoaded", async() => { 87 | const activeTab = await getActiveTabURL(); 88 | var container = document.getElementsByName("container")[0]; 89 | var slidercontainer = document.getElementsByName("slidecontainer")[0]; 90 | if (checkIsTwitter(activeTab)) { 91 | OnChangeLogo(); 92 | container.innerHTML = '작동중'; 93 | } else { 94 | containter.innerHTML = '트위터에서만 사용 가능합니다.' 95 | slidercontainer.style.display = 'none' 96 | } 97 | 98 | var slider = document.getElementById("DelayRange"); 99 | var output = document.getElementById("delayValue"); 100 | output.innerHTML = slider.value; 101 | 102 | slider.oninput = function() { 103 | output.innerHTML = this.value; 104 | } 105 | 106 | var hideElement = document.getElementById("hideElements"); 107 | hideElement.addEventListener('change', function() { 108 | chrome.storage.sync.set({ "hideElements": this.checked }, function(){ 109 | refreshCurrentTab() 110 | }); 111 | }); 112 | 113 | chrome.storage.sync.get(["hideElements"], function(items){ 114 | hideElement.checked = items["hideElements"]; 115 | }); 116 | 117 | 118 | var hideBlueMarkButton = document.getElementById("hideBlueMarkButton"); 119 | hideBlueMarkButton.addEventListener('change', function() { 120 | chrome.storage.sync.set({ "hideBlueMarkButton": this.checked }, function(){ 121 | refreshCurrentTab() 122 | }); 123 | }); 124 | 125 | chrome.storage.sync.get(["hideBlueMarkButton"], function(items){ 126 | hideBlueMarkButton.checked = items["hideBlueMarkButton"]; 127 | }); 128 | 129 | var hideBlueMark = document.getElementById("hideBlueMark"); 130 | hideBlueMark.addEventListener('change', function() { 131 | var value = this.checked 132 | chrome.storage.sync.set({ "hideBlueMark": this.checked }, function(){ 133 | if(value) 134 | hideBlueMarkButton.parentNode.style.display = 'flex' 135 | else 136 | hideBlueMarkButton.parentNode.style.display = 'none' 137 | 138 | refreshCurrentTab() 139 | }); 140 | }); 141 | 142 | chrome.storage.sync.get(["hideBlueMark"], function(items){ 143 | hideBlueMark.checked = items["hideBlueMark"]; 144 | 145 | if(items["hideBlueMark"]) 146 | hideBlueMarkButton.parentNode.style.display = 'flex' 147 | else 148 | hideBlueMarkButton.parentNode.style.display = 'none' 149 | }); 150 | 151 | 152 | if (checkIsTwitter(activeTab) && activeTab.url.includes("with_replies")) { 153 | const container = document.getElementsByName("container")[0]; 154 | container.innerHTML = ''; 155 | 156 | var fieldset = document.createElement('fieldset'); 157 | 158 | var legend = document.createElement('legend'); 159 | legend.textContent = '트윗 청소기 옵션'; 160 | fieldset.appendChild(legend); 161 | 162 | var keywordsDiv = document.createElement('div'); 163 | keywordsDiv.style.marginBottom = '10px'; 164 | var keywordsLabel = document.createElement('label'); 165 | keywordsLabel.textContent = '제외할 키워드 (쉼표로 구분)'; 166 | var keywordsInput = document.createElement('input'); 167 | keywordsInput.type = 'text'; 168 | keywordsInput.id = 'excludeKeywords'; 169 | keywordsInput.style.width = '100%'; 170 | keywordsDiv.appendChild(keywordsLabel); 171 | keywordsDiv.appendChild(keywordsInput); 172 | fieldset.appendChild(keywordsDiv); 173 | 174 | var dateRangeDiv = document.createElement('div'); 175 | dateRangeDiv.style.marginBottom = '10px'; 176 | 177 | var startDateDiv = document.createElement('div'); 178 | var startDateLabel = document.createElement('label'); 179 | startDateLabel.textContent = '시작 날짜: '; 180 | var startDateInput = document.createElement('input'); 181 | startDateInput.type = 'date'; 182 | startDateInput.id = 'startDate'; 183 | startDateDiv.appendChild(startDateLabel); 184 | startDateDiv.appendChild(startDateInput); 185 | 186 | var endDateDiv = document.createElement('div'); 187 | var endDateLabel = document.createElement('label'); 188 | endDateLabel.textContent = '종료 날짜: '; 189 | var endDateInput = document.createElement('input'); 190 | endDateInput.type = 'date'; 191 | endDateInput.id = 'endDate'; 192 | endDateDiv.appendChild(endDateLabel); 193 | endDateDiv.appendChild(endDateInput); 194 | 195 | dateRangeDiv.appendChild(startDateDiv); 196 | dateRangeDiv.appendChild(endDateDiv); 197 | fieldset.appendChild(dateRangeDiv); 198 | 199 | var checkbox1Div = document.createElement('div'); 200 | checkbox1Div.style.display = 'flex'; 201 | checkbox1Div.style.alignItems = 'center'; 202 | var checkbox1 = document.createElement('input'); 203 | checkbox1.type = 'checkbox'; 204 | checkbox1.id = 'checkbox1'; 205 | checkbox1.checked = true; 206 | checkbox1Div.appendChild(checkbox1); 207 | var label1 = document.createElement('label'); 208 | label1.textContent = '리트윗 지우기'; 209 | label1.style.marginLeft = '8px'; 210 | label1.setAttribute('for', 'checkbox1'); 211 | checkbox1Div.appendChild(label1); 212 | fieldset.appendChild(checkbox1Div); 213 | 214 | var checkbox2Div = document.createElement('div'); 215 | checkbox2Div.style.display = 'flex'; 216 | checkbox2Div.style.alignItems = 'center'; 217 | var checkbox2 = document.createElement('input'); 218 | checkbox2.type = 'checkbox'; 219 | checkbox2.id = 'checkbox2'; 220 | checkbox2.checked = true; 221 | checkbox2Div.appendChild(checkbox2); 222 | var label2 = document.createElement('label'); 223 | label2.textContent = '내 트윗 지우기'; 224 | label2.style.marginLeft = '8px'; 225 | label2.setAttribute('for', 'checkbox2'); 226 | checkbox2Div.appendChild(label2); 227 | fieldset.appendChild(checkbox2Div); 228 | 229 | checkbox2Div.style.display = 'none'; 230 | 231 | container.appendChild(fieldset); 232 | 233 | var button = document.createElement('button'); 234 | button.onclick = OnTweetClean; 235 | button.classList.add("button-17"); 236 | button.innerHTML = "트윗 청소하기"; 237 | button.style = "margin-top: 1rem;margin-bottom: 1rem"; 238 | container.appendChild(button); 239 | 240 | chrome.storage.sync.get([ 241 | "excludeKeywords" 242 | ], function(items) { 243 | if (items.excludeKeywords) keywordsInput.value = items.excludeKeywords; 244 | 245 | startDateInput.valueAsDate = new Date(0); 246 | const tomorrow = new Date(); 247 | tomorrow.setDate(tomorrow.getDate() + 1); 248 | tomorrow.setHours(0, 0, 0, 0); 249 | endDateInput.valueAsDate = tomorrow; 250 | }); 251 | 252 | keywordsInput.addEventListener('change', function() { 253 | chrome.storage.sync.set({ "excludeKeywords": this.value }); 254 | }); 255 | } else if ((checkIsTwitter(activeTab))&& activeTab.url.includes("likes")) { 256 | const container = document.getElementsByName("container")[0]; 257 | container.innerHTML = ''; 258 | const slidercontainer = document.getElementsByName("slidecontainer")[0]; 259 | slidercontainer.style.display = '' 260 | 261 | var button = document.createElement('button'); 262 | button.onclick = OnHeartClean; 263 | button.classList.add("button-17") 264 | button.innerHTML = "마음함 청소하기"; 265 | container.appendChild(button); 266 | } else { 267 | const slidercontainer = document.getElementsByName("slidecontainer")[0]; 268 | slidercontainer.style.display = 'none' 269 | const container = document.getElementsByName("container")[0]; 270 | container.innerHTML = '트윗 청소기를 사용하시려면 본인의 트윗 및 답글 페이지, 혹은 마음 페이지를 열어주세요.'; 271 | var button = document.getElementsByClassName("CleanButton")[0]; 272 | if (button != null) 273 | button.parentNode.removeChild(button); 274 | } 275 | }); -------------------------------------------------------------------------------- /delete_tweet.js: -------------------------------------------------------------------------------- 1 | var authorization = null; 2 | var ua = navigator.userAgentData.brands.map(brand => `"${brand.brand}";v="${brand.version}"`).join(', '); 3 | var client_tid = null; 4 | var client_uuid = null; 5 | var csrf_token = null; 6 | var platform = null; 7 | var random_resource = "OAx9yEcW3JA9bPo63pcYlA"; 8 | var random_resource_old_tweets = "H8OOoI-5ZE4NxgRr8lfyWg" 9 | var language_code = navigator.language.split("-")[0] 10 | var tweets_to_delete = [] 11 | var tweets_to_delete_text = [] 12 | var user_id = null; 13 | var username = null; 14 | var stop_signal = undefined 15 | var twitter_archive_content = undefined 16 | var twitter_archive_loading_confirmed = false 17 | var is_running = false 18 | var is_rate_limited = false 19 | var deletedCount = 0 20 | var retries = 0 21 | var id = null; 22 | var features = null; 23 | 24 | var log = "" 25 | 26 | const max_retries = 5 27 | 28 | function buildAcceptLanguageString() { 29 | const languages = navigator.languages; 30 | 31 | // Check if we have any languages 32 | if (!languages || languages.length === 0) { 33 | return "en-US,en;q=0.9"; // Default value if nothing is available 34 | } 35 | 36 | let q = 1; 37 | const decrement = 0.1; 38 | 39 | return languages.map(lang => { 40 | if (q < 1) { 41 | const result = `${lang};q=${q.toFixed(1)}`; 42 | q -= decrement; 43 | return result; 44 | } 45 | q -= decrement; 46 | return lang; 47 | }).join(','); 48 | } 49 | 50 | async function sleep(ms) { 51 | return new Promise(resolve => setTimeout(resolve, ms)); 52 | } 53 | 54 | async function fetch_tweets(options, cursor = null) { 55 | try { 56 | if (is_rate_limited) { 57 | if (options.statusCallback) { 58 | options.statusCallback("Rate limit reached. Waiting 3 minute..."); 59 | } 60 | await sleep(1000 * 180); 61 | is_rate_limited = false; 62 | } 63 | 64 | let count = "20"; 65 | let final_cursor = cursor ? `%22cursor%22%3A%22${cursor}%22%2C` : ""; 66 | let resource = id;// options["old_tweets"] ? random_resource_old_tweets : random_resource 67 | let endpoint = options["old_tweets"] ? "UserTweets" : "UserTweetsAndReplies" 68 | var base_url = `https://x.com/i/api/graphql/${resource}/${endpoint}`; 69 | 70 | var variable = `?variables=%7B%22userId%22%3A%22${user_id}%22%2C%22count%22%3A${count}%2C${final_cursor}%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withVoice%22%3Atrue%7D`; 71 | 72 | var final_url = `${base_url}${variable}&features=${features}`; 73 | 74 | const headers = { 75 | "accept": "*/*", 76 | "accept-language": buildAcceptLanguageString(), 77 | "authorization": authorization, 78 | "content-type": "application/json", 79 | "sec-ch-ua": ua, 80 | "sec-ch-ua-mobile": "?0", 81 | "sec-ch-ua-platform": platform, 82 | "sec-fetch-dest": "empty", 83 | "sec-fetch-mode": "cors", 84 | "sec-fetch-site": "same-origin", 85 | "x-client-transaction-id": client_tid, 86 | "x-csrf-token": csrf_token, 87 | "x-twitter-active-user": "yes", 88 | "x-twitter-auth-type": "OAuth2Session", 89 | "x-twitter-client-language": language_code 90 | }; 91 | 92 | // if (client_uuid) { 93 | // headers["x-client-uuid"] = client_uuid; 94 | // } 95 | 96 | 97 | 98 | const response = await fetch(final_url, { 99 | "headers": headers, 100 | "referrer": `https://x.com/${username}/with_replies`, 101 | "referrerPolicy": "strict-origin-when-cross-origin", 102 | "body": null, 103 | "method": "GET", 104 | "mode": "cors", 105 | "credentials": "include" 106 | }); 107 | 108 | log += "fetch " + final_url + " " + response.status + "\n" 109 | 110 | if (!response.ok) { 111 | retries = retries + 1 112 | 113 | if (response.status === 429) { 114 | is_rate_limited = true; 115 | return await fetch_tweets(options, cursor); 116 | } 117 | if (retries >= max_retries) { 118 | if (options.statusCallback) { 119 | options.statusCallback("Max retries reached. Please try again later."); 120 | } 121 | throw new Error("Max retries reached"); 122 | } 123 | console.log(`(fetch_tweets) Network response was not ok, retrying in ${5 * (1 + retries)} seconds`); 124 | 125 | options.statusCallback(`fetch ${retries}회 실패, ${5 * (1 + retries)}초 대기`) 126 | 127 | console.log(response.text()) 128 | 129 | await sleep(5000 * (1 + retries)); 130 | return await fetch_tweets(options, cursor) 131 | } 132 | 133 | retries = 0 134 | const data = await response.json(); 135 | var entries = data["data"]["user"]["result"]["timeline"]["timeline"]["instructions"] 136 | for (item of entries) { 137 | if (item["type"] == "TimelineAddEntries") { 138 | entries = item["entries"] 139 | } 140 | } 141 | console.log(entries); 142 | return entries; 143 | } catch (error) { 144 | if (options.statusCallback) { 145 | options.statusCallback(`Error fetching tweets: ${error.message}`); 146 | } 147 | throw error; 148 | } 149 | } 150 | 151 | async function log_tweets(options, entries) { 152 | for (let item of entries) { 153 | if (item["entryId"].startsWith("profile-conversation") || item["entryId"].startsWith("tweet-")) { 154 | findTweetIds(options, item) 155 | } 156 | else if (item["entryId"].startsWith("cursor-bottom") && entries.length > 2) { 157 | let cursor_bottom = item["content"]["value"]; 158 | 159 | return cursor_bottom; 160 | } 161 | } 162 | return "finished" 163 | } 164 | 165 | function check_keywords(options, text) { 166 | if (options["match_any_keywords"].length == 0) { 167 | return true 168 | } 169 | for (let word of options["match_any_keywords"]) { 170 | if (text.includes(word)) 171 | { 172 | console.log(options["match_any_keywords"], text) 173 | return false 174 | } 175 | } 176 | return true 177 | } 178 | 179 | function check_date(options, tweet) { 180 | if (tweet['legacy'].hasOwnProperty('created_at')) { 181 | tweet_date = new Date(tweet['legacy']["created_at"]) 182 | after_date = new Date(options["after_date"]) 183 | before_date = new Date(options["before_date"]) 184 | tweet_date.setHours(0, 0, 0, 0); 185 | if (tweet_date > after_date && tweet_date < before_date) { 186 | return true 187 | } 188 | else if (tweet_date < after_date) { 189 | stop_signal = true 190 | } 191 | console.log(tweet_date, after_date, before_date) 192 | return false 193 | } 194 | return true 195 | } 196 | 197 | function check_filter(options, tweet) { 198 | if (tweet['legacy'].hasOwnProperty('id_str') 199 | && ( options["tweets_to_ignore"].includes(tweet['legacy']["id_str"]) || options["tweets_to_ignore"].includes( parseInt(tweet['legacy']["id_str"]) ) )) { 200 | return false 201 | } 202 | if (options["delete_message_with_url_only"] == true) 203 | { 204 | if (tweet['legacy'].hasOwnProperty('entities') && tweet['legacy']["entities"].hasOwnProperty('urls') && tweet['legacy']["entities"]["urls"].length > 0 205 | && check_keywords(options, tweet['legacy']['full_text']) && check_date(options, tweet)) { 206 | return true 207 | } 208 | return false 209 | } 210 | if (check_keywords(options, tweet['legacy']['full_text']) && check_date(options, tweet)) 211 | return true 212 | return false 213 | } 214 | 215 | function check_tweet_owner(options, obj, uid) { 216 | if (obj.hasOwnProperty('legacy') && obj['legacy'].hasOwnProperty('retweeted') && obj['legacy']['retweeted'] === true && options["unretweet"] == false) 217 | return false 218 | if (obj.hasOwnProperty('user_id_str') && obj['user_id_str'] === uid) 219 | return true; 220 | else if (obj.hasOwnProperty('legacy') && obj['legacy'].hasOwnProperty('user_id_str') && obj['legacy']['user_id_str'] === uid) 221 | return true; 222 | return false 223 | } 224 | 225 | function tweetFound(obj,options) { 226 | tweets_to_delete_text.push(`${obj['legacy']['full_text']}`) 227 | // if (options.statusCallback) { 228 | // options.statusCallback(`삭제 : ${obj['legacy']['full_text']}`); 229 | // } 230 | } 231 | 232 | function findTweetIds(options, obj) { 233 | function recurse(currentObj) { 234 | if (typeof currentObj !== 'object' || currentObj === null 235 | || (options["do_not_remove_pinned_tweet"] == true && currentObj['__type'] == "TimelinePinEntry")) { 236 | return; 237 | } 238 | 239 | if (currentObj['__typename'] === 'TweetWithVisibilityResults' && currentObj.hasOwnProperty('tweet') 240 | && check_tweet_owner(options, currentObj['tweet'], user_id) && check_filter(options, currentObj['tweet'])) { 241 | tweets_to_delete.push(currentObj['tweet']['id_str'] || currentObj['tweet']['legacy']['id_str']); 242 | tweetFound(currentObj['tweet'], options) 243 | } 244 | 245 | else if (currentObj.hasOwnProperty('__typename') && currentObj['__typename'] === 'Tweet' 246 | && check_tweet_owner(options, currentObj, user_id) && check_filter(options, currentObj)) { 247 | tweets_to_delete.push(currentObj['id_str'] || currentObj['legacy']['id_str']); 248 | tweetFound(currentObj,options) 249 | } 250 | 251 | for (let key in currentObj) { 252 | if (currentObj.hasOwnProperty(key)) { 253 | recurse(currentObj[key]); 254 | } 255 | } 256 | } 257 | 258 | recurse(obj); 259 | } 260 | 261 | async function delete_tweets(id_list, options) { 262 | var delete_tid = "LuSa1GYxAMxWEugf+FtQ/wjCAUkipMAU3jpjkil3ujj7oq6munDCtNaMaFmZ8bcm7CaNvi4GIXj32jp7q32nZU8zc5CyLw" 263 | var id_list_size = id_list.length 264 | var retry = 0 265 | 266 | for (let i = 0; i < id_list_size; ++i) { 267 | const headers = { 268 | "accept": "*/*", 269 | "accept-language": buildAcceptLanguageString(), 270 | "authorization": authorization, 271 | "content-type": "application/json", 272 | "sec-ch-ua": ua, 273 | "sec-ch-ua-mobile": "?0", 274 | "sec-ch-ua-platform": platform, 275 | "sec-fetch-dest": "empty", 276 | "sec-fetch-mode": "cors", 277 | "sec-fetch-site": "same-origin", 278 | "x-client-transaction-id": delete_tid, 279 | "x-csrf-token": csrf_token, 280 | "x-twitter-active-user": "yes", 281 | "x-twitter-auth-type": "OAuth2Session", 282 | "x-twitter-client-language": language_code 283 | }; 284 | 285 | if (client_uuid) { 286 | headers["x-client-uuid"] = client_uuid; 287 | } 288 | 289 | const response = await fetch("https://x.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet", { 290 | "headers": headers, 291 | "referrer": `https://x.com/${username}/with_replies`, 292 | "referrerPolicy": "strict-origin-when-cross-origin", 293 | "body": `{\"variables\":{\"tweet_id\":\"${id_list[i]}\",\"dark_request\":false},\"queryId\":\"VaenaVgh5q5ih7kvyVjgtg\"}`, 294 | "method": "POST", 295 | "mode": "cors", 296 | "credentials": "include" 297 | }); 298 | 299 | log += "delete https://x.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet" + " for " + id_list[i] + " : " + response.status + "\n" 300 | 301 | if (!response.ok) { 302 | if (response.status === 429) { 303 | if (options.statusCallback) { 304 | options.statusCallback("Rate limit reached. Waiting 3 minute..."); 305 | } 306 | await sleep(1000 * 180); 307 | i -= 1; 308 | continue; 309 | } 310 | if (retry == 8) { 311 | if (options.statusCallback) { 312 | options.statusCallback("Max retries reached"); 313 | } 314 | throw new Error("Max retries reached"); 315 | } 316 | i -= 1; 317 | await sleep(10000 * (1 + retry)); 318 | continue; 319 | } 320 | 321 | retry = 0; 322 | deletedCount++; 323 | 324 | if (options.countCallback) { 325 | options.statusCallback(`삭제 : ${tweets_to_delete_text[i]}`) 326 | options.countCallback(deletedCount); 327 | } 328 | 329 | await sleep(50); 330 | } 331 | } 332 | 333 | async function run(options) { 334 | retries = 0 335 | log = "" 336 | authorization = options.headers.authorization; 337 | client_tid = options.headers.clientTid; 338 | client_uuid = options.headers.clientUuid; 339 | csrf_token = options.csrf_token; 340 | user_id = options.user_id; 341 | username = options.headers.username; 342 | platform = options.headers.platform 343 | id = options.headers.id 344 | features = options.headers.features; 345 | deletedCount = 0; 346 | tweets_to_delete = []; 347 | tweets_to_delete_text = []; 348 | 349 | var next = null; 350 | var entries = undefined; 351 | is_running = true; 352 | 353 | log += "Tweet Delete Log\n" 354 | log += "Initial Settings\n" + options.headers.id + "\n" 355 | log += "\n==========================\n" 356 | 357 | try { 358 | while (next != "finished" && stop_signal != true) { 359 | entries = await fetch_tweets(options, next); 360 | next = await log_tweets(options, entries); 361 | await delete_tweets(tweets_to_delete, options); 362 | tweets_to_delete = []; 363 | tweets_to_delete_text = []; 364 | await sleep(1000); 365 | } 366 | } catch (error) { 367 | log += error 368 | console.error("Error in run:", error); 369 | throw error 370 | } finally { 371 | is_running = false; 372 | } 373 | } -------------------------------------------------------------------------------- /contentScript.js: -------------------------------------------------------------------------------- 1 | var whiteList = new Set() 2 | var whiteListText = "" 3 | var whiteListFromFiles = new Set() 4 | 5 | fetch(chrome.runtime.getURL("data/whitelist.txt")) 6 | .then(response => response.text()) 7 | .then(text => { 8 | var lines = text.split(/\r?\n/); 9 | var cleanedLines = lines.map(line => line.replace(/[\s]+/g, '')); 10 | cleanedLines.forEach(item => whiteList.add(item)) 11 | cleanedLines.forEach(item => whiteListFromFiles.add(item)) 12 | }) 13 | .catch(error => console.error("Error fetching static file:", error)); 14 | 15 | const observer = new MutationObserver((mutationsList, observer) => { 16 | for (const mutation of mutationsList) { 17 | if (mutation.type === 'childList') { 18 | onDOMUpdate(); 19 | } 20 | } 21 | }); 22 | 23 | chrome.storage.sync.get(["whitelist"], function (items) { 24 | whiteListText = items["whitelist"]; 25 | if (!whiteListText) { 26 | whiteListText = "" 27 | return; 28 | } 29 | 30 | var lines = whiteListText.split(/\r?\n/); 31 | var cleanedLines = lines.map(line => line.replace(/[\s]+/g, '')); 32 | cleanedLines.forEach(item => whiteList.add(item)) 33 | }); 34 | 35 | var hideBlueMark = false 36 | var hideBlueMarkButton = false 37 | 38 | const targetNode = document.body; 39 | 40 | const config = { childList: true, subtree: true }; 41 | 42 | observer.observe(targetNode, config); 43 | 44 | 45 | const logo = "M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z" 46 | 47 | function sleep(ms) { 48 | return new Promise((r) => setTimeout(r, ms)); 49 | } 50 | 51 | function isWhiteList(id) { 52 | return whiteList.has(id); 53 | } 54 | 55 | function addWhiteList(id) { 56 | whiteListText += "\n" + id; 57 | whiteList.add(id) 58 | chrome.storage.sync.set({ "whitelist": whiteListText }, function () { 59 | }); 60 | } 61 | 62 | function removeWhiteList(id) { 63 | const lines = whiteListText.split('\n'); 64 | const filteredLines = lines.filter(line => line.trim() !== id); 65 | const newText = filteredLines.join('\n'); 66 | whiteListText = newText; 67 | whiteList.delete(id) 68 | chrome.storage.sync.set({ "whitelist": whiteListText }, function () { 69 | }); 70 | } 71 | 72 | async function findObject(findQuery, time = 100, interval = 100) { 73 | var find = document.querySelectorAll(findQuery); 74 | if (find.length == 0) { 75 | for (var i = 0; i < time; i++) { 76 | find = document.querySelectorAll(findQuery); 77 | if (find.length != 0) 78 | break; 79 | await sleep(interval); 80 | } 81 | } 82 | return find[0] 83 | } 84 | 85 | async function findObjectFrom(findQuery, from) { 86 | var find = from.querySelectorAll(findQuery); 87 | if (find.length == 0) { 88 | for (var i = 0; i < 100; i++) { 89 | find = from.querySelectorAll(findQuery); 90 | if (find.length != 0) 91 | break; 92 | await sleep(100); 93 | } 94 | } 95 | return find[0] 96 | } 97 | 98 | 99 | 100 | async function findObjectAll(findQuery) { 101 | var find = document.querySelectorAll(findQuery); 102 | if (find.length == 0) { 103 | for (var i = 0; i < 100; i++) { 104 | find = document.querySelectorAll(findQuery); 105 | if (find.length != 0) 106 | break; 107 | await sleep(100); 108 | } 109 | } 110 | return find 111 | } 112 | 113 | function waitForNonNullAsync(objGetter, interval = 100) { 114 | return new Promise((resolve) => { 115 | const checkInterval = setInterval(() => { 116 | if (objGetter() !== null) { 117 | clearInterval(checkInterval); 118 | resolve(objGetter()); 119 | } 120 | }, interval); 121 | }); 122 | } 123 | 124 | function getID(target) { 125 | var finalName = "error" 126 | var userName = target.querySelector('[data-testid="User-Name"]') 127 | if (userName) { 128 | var idHints = userName.querySelectorAll('[style="text-overflow: unset;"]') 129 | var atStartingElements = Array.from(idHints).filter(element => 130 | element.innerHTML.trim().startsWith('@') 131 | ); 132 | finalName = atStartingElements[0].innerHTML 133 | } 134 | return finalName 135 | } 136 | 137 | async function attachBlueBox(target, id) { 138 | if (target.querySelectorAll('[id="clickToSeeButton"]').length != 0) 139 | return; 140 | 141 | var original = target.firstChild 142 | original.style.display = 'none' 143 | var button = document.createElement('div'); 144 | button.style.textAlign = 'center' 145 | button.id = "clickToSeeButton" 146 | button.innerHTML = '파란 딱지 [' + id + '] 의 트윗 숨겨짐 : 눌러서 트윗 열기' 147 | button.onclick = () => { 148 | if (original.style.display === 'none') { 149 | original.style.display = '' 150 | button.innerHTML = '파란 딱지 [' + id + '] 의 트윗 보여지는 중 : 눌러서 트윗 숨기기' 151 | } 152 | else { 153 | original.style.display = 'none' 154 | button.innerHTML = '파란 딱지 [' + id + '] 의 트윗 숨겨짐 : 눌러서 트윗 열기' 155 | } 156 | } 157 | target.prepend(button); 158 | } 159 | 160 | 161 | 162 | async function onDOMUpdate() { 163 | if (!hideBlueMark) 164 | return; 165 | 166 | var timeline = await findObject('[aria-label^="타임라인"]'); 167 | 168 | if (!timeline) 169 | return; 170 | 171 | if (!whiteList) { 172 | await waitForNonNullAsync(() => whiteList, 1); 173 | } 174 | 175 | var cellInnerDivs = timeline.querySelectorAll('[data-testid="cellInnerDiv"]'); 176 | 177 | for (var i = 0; i < cellInnerDivs.length; i++) { 178 | var current = cellInnerDivs[i]; 179 | 180 | if (current.querySelectorAll('[aria-label="인증된 계정"]').length != 0) { 181 | var id = getID(current) 182 | 183 | if (isWhiteList(id)) 184 | continue; 185 | 186 | if (hideBlueMarkButton) { 187 | current.innerHTML = '' 188 | continue; 189 | } 190 | 191 | attachBlueBox(current, id) 192 | current.style.border = '1px solid #303030' 193 | } 194 | } 195 | } 196 | 197 | function OnHideBlueMark(message) { 198 | hideBlueMark = message.hideBlueMark 199 | hideBlueMarkButton = message.hideBlueMarkButton 200 | } 201 | 202 | 203 | async function makeWhiteListButton() { 204 | var isTimeline = await findObject('[aria-label="프로필 타임라인"]', 100, 10); 205 | if (!isTimeline) { 206 | return; 207 | } 208 | 209 | var button = await findObject('[aria-label="더 보기"]') 210 | if (!button) { 211 | return; 212 | } 213 | 214 | var checkButton = document.querySelectorAll('[id="whiteListButton"]') 215 | if (checkButton.length != 0) { 216 | while (checkButton.length > 0) { 217 | if (checkButton[0].parentNode) 218 | checkButton[0].parentElement.removeChild(checkButton[0]); 219 | else 220 | break; 221 | } 222 | } 223 | 224 | var profile = await findObject('[data-testid="UserName"]') 225 | if (!profile) { 226 | return; 227 | } 228 | 229 | var profilespans = profile.getElementsByTagName('span'); 230 | var id = '' 231 | for (var i = 0; i < profilespans.length; i++) { 232 | if (profilespans[i].innerHTML.startsWith('@')) 233 | id = profilespans[i].innerHTML; 234 | } 235 | 236 | var buttonParent = button; 237 | var buttonParentParent = buttonParent.parentNode; 238 | var newButton = buttonParent.cloneNode(true); 239 | newButton.id = 'whiteListButton' 240 | var svgs = newButton.getElementsByTagName('svg'); 241 | while (svgs.length > 0) { 242 | svgs[0].parentNode.removeChild(svgs[0]); 243 | } 244 | 245 | var spans = newButton.getElementsByTagName('span'); 246 | var buttonInnerSpan = spans[0] 247 | 248 | if (isWhiteList(id)) { 249 | buttonInnerSpan.innerHTML = "화이트리스트 O" 250 | buttonInnerSpan.parentNode.parentNode.style.backgroundColor = "white" 251 | buttonInnerSpan.style.color = "black" 252 | } 253 | else { 254 | buttonInnerSpan.innerHTML = "화이트리스트 X" 255 | buttonInnerSpan.parentNode.parentNode.style.backgroundColor = "black" 256 | buttonInnerSpan.style.color = "white" 257 | } 258 | 259 | buttonInnerSpan.style.paddingLeft = '10px' 260 | buttonInnerSpan.style.paddingRight = '10px' 261 | 262 | newButton.onclick = () => { 263 | var checkIsWhiteList = isWhiteList(id) 264 | if (checkIsWhiteList) { 265 | if (whiteListFromFiles.has(id)) { 266 | alert("파일에서 추가된 화이트리스트는 버튼으로 제거할 수 없습니다. data/whitelist.txt 파일에서 제거해주세요.") 267 | return; 268 | } 269 | removeWhiteList(id) 270 | } 271 | else 272 | addWhiteList(id) 273 | 274 | if (!checkIsWhiteList) { 275 | buttonInnerSpan.innerHTML = "화이트리스트 O" 276 | buttonInnerSpan.parentNode.parentNode.style.backgroundColor = "white" 277 | buttonInnerSpan.style.color = "black" 278 | } 279 | else { 280 | buttonInnerSpan.innerHTML = "화이트리스트 X" 281 | buttonInnerSpan.parentNode.parentNode.style.backgroundColor = "black" 282 | buttonInnerSpan.style.color = "white" 283 | } 284 | 285 | } 286 | 287 | buttonParentParent.insertBefore(newButton, buttonParent.parentNode.lastElementChild); 288 | } 289 | 290 | async function changeLogo(message) { 291 | if (!message.hideElements) 292 | return 293 | element = await findObject('[rel="shortcut icon"]'); 294 | if (element) { 295 | var originalElement = element; 296 | var clonedElement = originalElement.cloneNode(true); 297 | clonedElement.href = "//abs.twimg.com/favicons/twitter.2.ico" 298 | clonedElement.id = "new icon"; 299 | var parentElement = originalElement.parentNode; 300 | parentElement.insertBefore(clonedElement, originalElement.nextSibling); 301 | parentElement.removeChild(originalElement); 302 | } 303 | 304 | var element = await findObject('[href="/home"]'); 305 | if (element.childNodes.length > 0) { 306 | const pathElement = element.childNodes[0].childNodes[0].childNodes[0].childNodes[0]; 307 | 308 | if (pathElement) { 309 | pathElement.setAttribute('d', logo); 310 | } 311 | } 312 | 313 | element = await findObject('[aria-label="그록"]'); 314 | if (element) { 315 | element.style.cssText = 'display: None;' 316 | } 317 | 318 | element = await findObject('[aria-label="Premium"]'); 319 | if (element) { 320 | element.style.cssText = 'display: None;' 321 | } 322 | 323 | element = await findObject('[aria-label="커뮤니티"]'); 324 | if (element) { 325 | element.style.cssText = 'display: None;' 326 | } 327 | 328 | element = await findObject('[aria-label="Premium 구독하기"]'); 329 | if (element) { 330 | element.style.cssText = 'display: None;' 331 | } 332 | 333 | element = await findObject('[aria-label="인증된 조직"]'); 334 | if (element) { 335 | element.style.cssText = 'display: None;' 336 | } 337 | 338 | element = await findObject('[aria-label="채용"]'); 339 | if (element) { 340 | element.style.cssText = 'display: None;' 341 | } 342 | 343 | } 344 | 345 | async function makeButton() { 346 | const currentUrl = window.location.href; 347 | if (currentUrl.endsWith("quotes")) 348 | return; 349 | 350 | var retweets = await findObjectAll('[data-testid="retweet"], [data-testid="unretweet"]'); 351 | var retweet = null; 352 | for (var i = 0; i < retweets.length; i++) { 353 | var now = retweets[i]; 354 | var target = now.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode; 355 | // alert(target.innerHTML) 356 | if (target.getAttribute('tabindex') === "-1") { 357 | retweet = retweets[i]; 358 | break; 359 | } 360 | } 361 | if (retweet == null) { 362 | await sleep(200); 363 | makeButton(); 364 | return; 365 | } 366 | 367 | var newNode = retweet.parentNode.cloneNode(true); 368 | retweet.parentNode.after(newNode); 369 | 370 | var retweetHolder = newNode.childNodes[0].childNodes[0].childNodes[0]; 371 | retweetHolder.parentNode.removeChild(retweetHolder); 372 | var textHolder = newNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0]; 373 | textHolder.innerHTML = "인용 보기"; 374 | 375 | newNode.addEventListener('click', async function (event) { 376 | var target = newNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode; 377 | var caret = await findObjectFrom('[data-testid="caret"]', target); 378 | caret.click(); 379 | var quotes = await findObject('[data-testid="tweetEngagements"]'); 380 | quotes.click(); 381 | }); 382 | } 383 | 384 | async function makeRandomButton() { 385 | var doesRandomButtonExists = document.querySelectorAll('[id="randomButton"]'); 386 | if (doesRandomButtonExists.length > 0) 387 | return; 388 | 389 | var backButton = await findObject('[data-testid="app-bar-back"]'); 390 | var newButton = backButton.parentNode.cloneNode(true); 391 | backButton.parentNode.parentNode.childNodes[1].after(newButton); 392 | newButton.id = "randomButton"; 393 | var pathElement = newButton.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0]; 394 | 395 | if (pathElement) { 396 | pathElement.setAttribute('d', "M7.0498 7.0498H7.0598M10.5118 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V10.5118C3 11.2455 3 11.6124 3.08289 11.9577C3.15638 12.2638 3.27759 12.5564 3.44208 12.8249C3.6276 13.1276 3.88703 13.387 4.40589 13.9059L9.10589 18.6059C10.2939 19.7939 10.888 20.388 11.5729 20.6105C12.1755 20.8063 12.8245 20.8063 13.4271 20.6105C14.112 20.388 14.7061 19.7939 15.8941 18.6059L18.6059 15.8941C19.7939 14.7061 20.388 14.112 20.6105 13.4271C20.8063 12.8245 20.8063 12.1755 20.6105 11.5729C20.388 10.888 19.7939 10.2939 18.6059 9.10589L13.9059 4.40589C13.387 3.88703 13.1276 3.6276 12.8249 3.44208C12.5564 3.27759 12.2638 3.15638 11.9577 3.08289C11.6124 3 11.2455 3 10.5118 3ZM7.5498 7.0498C7.5498 7.32595 7.32595 7.5498 7.0498 7.5498C6.77366 7.5498 6.5498 7.32595 6.5498 7.0498C6.5498 6.77366 6.77366 6.5498 7.0498 6.5498C7.32595 6.5498 7.5498 6.77366 7.5498 7.0498Z"); 397 | } 398 | 399 | newButton.addEventListener('click', async function (event) { 400 | OnRandom(); 401 | }); 402 | 403 | var newButton = backButton.parentNode.cloneNode(true); 404 | backButton.parentNode.parentNode.childNodes[1].after(newButton); 405 | newButton.id = "staticsButton"; 406 | var pathElement = newButton.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0]; 407 | 408 | if (pathElement) { 409 | pathElement.setAttribute('d', "M3 4.5C3 3.12 4.12 2 5.5 2h13C19.88 2 21 3.12 21 4.5v15c0 1.38-1.12 2.5-2.5 2.5h-13C4.12 22 3 20.88 3 19.5v-15zM5.5 4c-.28 0-.5.22-.5.5v15c0 .28.22.5.5.5h13c.28 0 .5-.22.5-.5v-15c0-.28-.22-.5-.5-.5h-13zM16 10H8V8h8v2zm-8 2h8v2H8v-2z"); 410 | } 411 | 412 | newButton.addEventListener('click', async function (event) { 413 | OnRetweetStatics(); 414 | }); 415 | } 416 | 417 | //random rt 418 | 419 | function getRandomInt(min, max) { 420 | min = Math.ceil(min); 421 | max = Math.floor(max); 422 | return Math.floor(Math.random() * (max - min)) + min; //최댓값은 제외, 최솟값은 포함 423 | } 424 | 425 | function getRandomItem(set) { 426 | let items = Array.from(set); 427 | return items[Math.floor(Math.random() * items.length)]; 428 | } 429 | 430 | async function OnRandom() { 431 | const initialUrl = window.location.href; 432 | if (!initialUrl.endsWith("retweets")) { 433 | alert("추첨은 리트윗 페이지에서만 가능합니다."); 434 | return; 435 | } 436 | 437 | alert("추첨 시작"); 438 | 439 | var element = await findObject('[aria-label="타임라인: 재게시"]'); 440 | 441 | var prevY = 0; 442 | 443 | window.scroll(0, 0); 444 | 445 | await sleep(500); 446 | 447 | while (true) { 448 | 449 | if (initialUrl != window.location.href) { 450 | alert("추첨 소됨"); 451 | return; 452 | } 453 | 454 | var currentY = window.scrollY + 500; 455 | 456 | if (currentY == prevY) 457 | break; 458 | 459 | window.scroll(0, currentY); 460 | 461 | 462 | await sleep(500); 463 | 464 | prevY = currentY; 465 | } 466 | 467 | window.scroll(0, getRandomInt(0, prevY)); 468 | await sleep(500); 469 | 470 | 471 | var find = element.querySelectorAll('[data-testid="cellInnerDiv"]'); 472 | var numbers = new Set(); 473 | for (var i = 0; i < find.length; i++) { 474 | numbers.add(i); 475 | } 476 | 477 | var index = getRandomItem(numbers); 478 | 479 | if (find[index].getElementsByClassName('span').length == 0) { 480 | numbers.delete(index); 481 | index = getRandomItem(numbers); 482 | } 483 | 484 | find[index].scrollIntoView(); 485 | find[index].style.backgroundColor = "dimgrey"; 486 | 487 | window.scroll(0, window.scrollY - 150); 488 | await sleep(200); 489 | alert("추첨 완료!"); 490 | } 491 | 492 | 493 | async function OnRetweetStatics() { 494 | const initialUrl = window.location.href; 495 | if (!initialUrl.endsWith("retweets")) { 496 | alert("RT 목록 취합은 리트윗 페이지에서만 가능합니다."); 497 | return; 498 | } 499 | 500 | alert("RT 목록 취합 시작"); 501 | 502 | var element = await findObject('[aria-label="타임라인: 재게시"]'); 503 | 504 | var prevY = 0; 505 | 506 | var retweeters = new Set(); 507 | window.scroll(0, 0); 508 | 509 | await sleep(500); 510 | 511 | while (true) { 512 | 513 | if (initialUrl != window.location.href) { 514 | alert("취합 취소됨"); 515 | return; 516 | } 517 | 518 | var currentY = window.scrollY + 500; 519 | 520 | if (currentY == prevY) 521 | break; 522 | 523 | window.scroll(0, currentY); 524 | 525 | await sleep(500); 526 | 527 | var foundRetweets = element.querySelectorAll('[data-testid="cellInnerDiv"]'); 528 | for (var i = 0; i < foundRetweets.length; i++) { 529 | retweeters.add(foundRetweets[i]); 530 | } 531 | 532 | prevY = currentY; 533 | } 534 | 535 | 536 | let retweetersArray = Array.from(retweeters); 537 | 538 | var statics = []; 539 | var log = ""; 540 | log += retweetersArray.length; 541 | log += "retweets.
"; 542 | 543 | 544 | var removed = new Set(); 545 | 546 | for (var i = 0; i < retweetersArray.length; i++) { 547 | var result = []; 548 | 549 | var spans = retweetersArray[i].querySelectorAll("span"); 550 | for (var j = 0; j < spans.length; j++) { 551 | if (!!spans[j].innerHTML) { 552 | if (removed.has(spans[j].parentNode)) 553 | continue; 554 | 555 | 556 | var findInnerImg = spans[j].querySelectorAll("img"); 557 | 558 | if (findInnerImg.length > 0) { 559 | var finalName = ""; 560 | var childs = findInnerImg[0].parentNode.childNodes; 561 | 562 | for (var k = 0; k < childs.length; k++) { 563 | console.log(childs[k]); 564 | console.log(childs[k].tagName); 565 | 566 | if (childs[k].tagName === "SPAN") 567 | finalName += childs[k].innerHTML; 568 | if (childs[k].tagName === "IMG") 569 | finalName += childs[k].alt; 570 | } 571 | 572 | result.push(finalName); 573 | log += finalName; 574 | log += " | "; 575 | removed.add(findInnerImg[0].parentNode); 576 | continue; 577 | } 578 | 579 | var findInnerSpan = spans[j].querySelectorAll("span"); 580 | 581 | if (findInnerSpan.length > 0) { 582 | continue; 583 | } 584 | 585 | var findInnerA = spans[j].querySelectorAll("a"); 586 | 587 | if (findInnerA.length > 0) { 588 | result.push(findInnerA[0].innerHTML); 589 | log += findInnerA[0].innerHTML; 590 | log += " | "; 591 | removed.add(findInnerA[0].parentNode); 592 | continue; 593 | } 594 | 595 | var findInnerSvg = spans[j].querySelectorAll("svg"); 596 | 597 | if (findInnerSvg.length > 0) { 598 | continue; 599 | } 600 | 601 | 602 | result.push(spans[j].innerHTML); 603 | log += spans[j].innerHTML; 604 | log += " | "; 605 | removed.add(spans[j].parentNode); 606 | } 607 | } 608 | 609 | log += "
========================
"; 610 | statics.push(result); 611 | } 612 | 613 | var newWindow = window.open('', '_blank'); 614 | 615 | var output = '\ 616 | \ 617 | \ 618 | \ 619 | \ 620 | \ 621 | '; 622 | 623 | 624 | for (var i = 0; i < statics.length - 1; i++) { 625 | output += ""; 626 | 627 | var current = statics[i]; 628 | output += ''; 629 | output += ''; 630 | if (current[2] === "나를 팔로우합니다") { 631 | output += ""; 632 | if (current[3] === "팔로잉") 633 | output += ""; 634 | else 635 | output += ""; 636 | } 637 | else { 638 | output += ""; 639 | if (current[2] === "팔로잉") 640 | output += ""; 641 | else 642 | output += ""; 643 | } 644 | 645 | output += ""; 646 | } 647 | 648 | output += "
이름ID상대가 나를 팔로우내가 상대를 팔로우
' + current[0] + '' + current[1] + 'OOXXOX
"; 649 | 650 | output += "



"; 651 | newWindow.document.write(output); 652 | //newWindow.document.write(log); 653 | } 654 | 655 | function getCookie(name) { 656 | const value = `; ${document.cookie}`; 657 | const parts = value.split(`; ${name}=`); 658 | if (parts.length === 2) return parts.pop().split(';').shift(); 659 | } 660 | 661 | async function OnTweetClean(message) { 662 | const overlay = document.createElement('div'); 663 | overlay.id = 'tweet-clean-overlay'; // ID 추가 664 | overlay.style.cssText = ` 665 | position: fixed; 666 | top: 0; 667 | left: 0; 668 | width: 100%; 669 | height: 100%; 670 | background-color: rgba(0, 0, 0, 0.9); 671 | display: flex; 672 | flex-direction: column; 673 | justify-content: center; 674 | align-items: center; 675 | z-index: 9999; 676 | `; 677 | 678 | const messageText = document.createElement('div'); 679 | messageText.style.cssText = ` 680 | color: white; 681 | font-size: 24px; 682 | font-weight: bold; 683 | text-align: center; 684 | margin-bottom: 20px; 685 | `; 686 | messageText.textContent = '트윗 청소중...'; 687 | 688 | const actionButton = document.createElement('button'); 689 | actionButton.style.cssText = ` 690 | padding: 10px 20px; 691 | font-size: 16px; 692 | background-color: #e74c3c; 693 | color: white; 694 | border: none; 695 | border-radius: 5px; 696 | cursor: pointer; 697 | transition: background-color 0.2s; 698 | `; 699 | actionButton.textContent = '취소'; 700 | actionButton.addEventListener('mouseover', () => { 701 | actionButton.style.backgroundColor = '#c0392b'; 702 | }); 703 | actionButton.addEventListener('mouseout', () => { 704 | actionButton.style.backgroundColor = '#e74c3c'; 705 | }); 706 | actionButton.addEventListener('click', () => { 707 | overlay.remove(); 708 | window.location.reload(); 709 | }); 710 | 711 | const statusText = document.createElement('div'); 712 | statusText.style.cssText = ` 713 | color: #cccccc; 714 | font-size: 16px; 715 | text-align: center; 716 | margin: 10px 0 20px 0; 717 | max-width: 80%; 718 | word-wrap: break-word; 719 | `; 720 | statusText.textContent = '시작 중...'; 721 | 722 | const countText = document.createElement('div'); 723 | countText.style.cssText = ` 724 | color: #cccccc; 725 | font-size: 16px; 726 | text-align: center; 727 | margin-bottom: 20px; 728 | `; 729 | countText.textContent = '삭제된 트윗: 0개'; 730 | 731 | overlay.appendChild(messageText); 732 | overlay.appendChild(actionButton); 733 | overlay.insertBefore(statusText, actionButton); 734 | overlay.insertBefore(countText, actionButton); 735 | 736 | document.body.appendChild(overlay); 737 | 738 | let deletedCount = 0; 739 | 740 | const delete_options = { 741 | "old_tweets": false, 742 | "unretweet": message.deleteRetweet, 743 | "delete_message_with_url_only": false, 744 | "match_any_keywords": message.excludeKeywords || [], 745 | "tweets_to_ignore": [], 746 | "after_date": message.startDate || new Date(0), 747 | "before_date": message.endDate || new Date(), 748 | "do_not_remove_pinned_tweet": false, 749 | "statusCallback": (status) => { 750 | console.log(status) 751 | statusText.textContent = status; 752 | }, 753 | "countCallback": (count) => { 754 | deletedCount = count; 755 | countText.textContent = `삭제된 트윗: ${count}개`; 756 | } 757 | }; 758 | 759 | try { 760 | const response = await new Promise((resolve) => { 761 | chrome.runtime.sendMessage({ type: "getHeaders" }, resolve); 762 | }); 763 | 764 | if (!response.headers || !response.headers.authorization || !response.headers.clientTid || !response.headers.id) { 765 | messageText.textContent = "필요한 인증 정보를 수집 중입니다. 잠시 후 다시 시도해주세요."; 766 | actionButton.textContent = '확인'; 767 | return; 768 | } 769 | 770 | const pathSegments = window.location.pathname.split('/'); 771 | const currentUsername = pathSegments[1]; 772 | 773 | const runOptions = { 774 | ...delete_options, 775 | headers: { 776 | ...response.headers, 777 | username: currentUsername 778 | }, 779 | csrf_token: getCookie("ct0"), 780 | user_id: getCookie("twid").substring(4) 781 | }; 782 | 783 | await run(runOptions); 784 | //await sleep(5000); 785 | 786 | messageText.textContent = '트윗 청소가 완료되었습니다!'; 787 | statusText.textContent = `총 ${deletedCount}개의 트윗이 삭제되었습니다.`; 788 | countText.style.display = 'none'; 789 | actionButton.textContent = '확인'; 790 | console.log(log) 791 | 792 | } catch (error) { 793 | console.log(log) 794 | console.error("트윗 삭제 중 오류 발생:", error); 795 | messageText.textContent = "트윗 청소 중 오류가 발생했습니다."; 796 | actionButton.textContent = '확인'; 797 | 798 | const newWin = window.open("", "_blank"); 799 | 800 | if (newWin) { 801 | newWin.document.title = "Error Viewer"; 802 | 803 | const style = newWin.document.createElement('style'); 804 | style.textContent = ` 805 | body { 806 | font-family: sans-serif; 807 | padding: 2rem; 808 | white-space: pre-wrap; 809 | } 810 | `; 811 | newWin.document.head.appendChild(style); 812 | 813 | const div = newWin.document.createElement('div'); 814 | div.textContent = log; 815 | newWin.document.body.appendChild(div); 816 | } else { 817 | alert("팝업 차단 때문에 새 창을 열 수 없습니다."); 818 | } 819 | } 820 | } 821 | 822 | async function OnHeartClean(message) { 823 | 824 | 825 | var skipSet = new Set(); 826 | var totalDeleteCount = 0 827 | 828 | var timelineElement = document.querySelectorAll('[aria-label^="타임라인:"]'); 829 | if (timelineElement.length == 0) { 830 | throw ("Timeline find failed"); 831 | } 832 | 833 | while (true) { 834 | var cellInnverDives = timelineElement[0].querySelectorAll('[data-testid="cellInnerDiv"]'); 835 | 836 | for (var i = 0; i < cellInnverDives.length; i++) { 837 | if (skipSet.has(cellInnverDives[i])) 838 | continue 839 | skipSet.add(cellInnverDives[i]) 840 | var unlike = timelineElement[0].querySelectorAll('[data-testid="unlike"]'); 841 | if (unlike.length == 0) { 842 | continue; 843 | } 844 | for (var i = 0; i < unlike.length; i++) { 845 | unlike[i].click(); 846 | totalDeleteCount++; 847 | } 848 | } 849 | 850 | var isScrolled = false 851 | for (var i = 0; i < 10; i++) { 852 | var beforeScroll = window.scrollY 853 | window.focus(); 854 | window.scrollBy(0, 500); 855 | await sleep(message.delay); 856 | var nowScroll = window.scrollY 857 | 858 | if (beforeScroll != nowScroll) { 859 | isScrolled = true 860 | break 861 | } 862 | } 863 | 864 | if (!isScrolled) 865 | break 866 | } 867 | 868 | alert("마음 삭제 완료!"); 869 | } 870 | 871 | 872 | 873 | chrome.runtime.onMessage.addListener((obj, sender, response) => { 874 | if (obj === "makeButton") 875 | makeButton(); 876 | else if (obj === "makeRandomButton") 877 | makeRandomButton(); 878 | else if (obj.isDeleteHeart) 879 | OnHeartClean(obj); 880 | else if (obj.deleteMytweet || obj.deleteRetweet) 881 | OnTweetClean(obj); 882 | else if (obj.type === "changeLogo") { 883 | changeLogo(obj); 884 | makeWhiteListButton(); 885 | } 886 | else if (obj.type === "hideBlueMark") 887 | OnHideBlueMark(obj) 888 | }); 889 | --------------------------------------------------------------------------------