├── README.md ├── LICENSE ├── Twitter-DeCensor-Chromium.user.js └── Twitter-DeCensor-Firefox.user.js /README.md: -------------------------------------------------------------------------------- 1 | # Twitter-DeCensor 2 | decensors your twitter 3 | 4 | (i know its vibecoded but i dont care) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 GitHub, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Twitter-DeCensor-Chromium.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Twitter DeCensor 3 | // @namespace http://tampermonkey.net/ 4 | // @version 1.0 5 | // @description where my tweets at. 6 | // @icon https://barachlo.szprink.xyz/fiIAlnosGV5w.png 7 | // @match https://twitter.com/* 8 | // @match https://x.com/* 9 | // @run-at document-start 10 | // @grant none 11 | // ==/UserScript== 12 | 13 | (function() { 14 | 'use strict'; 15 | 16 | // Helper to recursively remove mediaVisibilityResults key 17 | function recursiveClean(obj) { 18 | if (obj && typeof obj === "object") { 19 | if ("mediaVisibilityResults" in obj) { 20 | console.log("[PATCH] Removing mediaVisibilityResults from:", obj); 21 | delete obj.mediaVisibilityResults; 22 | } 23 | for (const key in obj) { 24 | recursiveClean(obj[key]); 25 | } 26 | } 27 | } 28 | 29 | // -------- Hook XHR -------- 30 | 31 | const realXHROpen = XMLHttpRequest.prototype.open; 32 | XMLHttpRequest.prototype.open = function(method, url) { 33 | this._url = url; // store URL for logging 34 | 35 | this.addEventListener('readystatechange', () => { 36 | if (this.readyState === 4 && this.responseType === '' && this.responseText) { 37 | try { 38 | // Try parse JSON 39 | let json = JSON.parse(this.responseText); 40 | recursiveClean(json); 41 | const patched = JSON.stringify(json); 42 | 43 | // Override responseText and response with patched version 44 | Object.defineProperty(this, 'responseText', { writable: true }); 45 | Object.defineProperty(this, 'response', { writable: true }); 46 | this.responseText = this.response = patched; 47 | 48 | console.log(`[XHR PATCHED] ${this._url}`); 49 | 50 | } catch (e) { 51 | // Not JSON, ignore 52 | } 53 | } 54 | }); 55 | return realXHROpen.apply(this, arguments); 56 | }; 57 | 58 | // -------- Hook fetch -------- 59 | 60 | const realFetch = window.fetch; 61 | window.fetch = async function(resource, init) { 62 | const response = await realFetch(resource, init); 63 | 64 | // Only patch Twitter API graphql calls 65 | if (typeof resource === 'string' && resource.includes('/i/api/graphql/')) { 66 | try { 67 | const cloned = response.clone(); 68 | const data = await cloned.json(); 69 | 70 | recursiveClean(data); 71 | 72 | const patchedBody = JSON.stringify(data); 73 | 74 | // Return a new Response with patched body, preserving original headers/status 75 | return new Response(patchedBody, { 76 | status: response.status, 77 | statusText: response.statusText, 78 | headers: response.headers, 79 | }); 80 | } catch (e) { 81 | // JSON parse failed, fallback to original response 82 | return response; 83 | } 84 | } else { 85 | return response; 86 | } 87 | }; 88 | })(); 89 | -------------------------------------------------------------------------------- /Twitter-DeCensor-Firefox.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Twitter DeCensor 3 | // @namespace http://tampermonkey.net/ 4 | // @version 1.0 5 | // @description where my tweets at 6 | // @icon https://barachlo.szprink.xyz/fiIAlnosGV5w.png 7 | // @match https://twitter.com/* 8 | // @match https://x.com/* 9 | // @run-at document-start 10 | // @grant none 11 | // ==/UserScript== 12 | 13 | (function() { 14 | 'use strict'; 15 | 16 | const pageCode = ` 17 | (function() { 18 | console.log("[DeCensor] Injected into page context"); 19 | 20 | function recursiveClean(obj) { 21 | if (obj && typeof obj === "object") { 22 | if ("mediaVisibilityResults" in obj) { 23 | console.log("[PATCH] Removing mediaVisibilityResults from:", obj); 24 | delete obj.mediaVisibilityResults; 25 | } 26 | for (const key in obj) { 27 | recursiveClean(obj[key]); 28 | } 29 | } 30 | } 31 | 32 | // -------- Hook XHR -------- 33 | const realXHROpen = XMLHttpRequest.prototype.open; 34 | XMLHttpRequest.prototype.open = function(method, url) { 35 | this._url = url; 36 | 37 | this.addEventListener('readystatechange', () => { 38 | if (this.readyState === 4 && this.responseType === '' && this.responseText) { 39 | try { 40 | let json = JSON.parse(this.responseText); 41 | recursiveClean(json); 42 | const patched = JSON.stringify(json); 43 | 44 | Object.defineProperty(this, 'responseText', { value: patched }); 45 | Object.defineProperty(this, 'response', { value: patched }); 46 | 47 | console.log(\`[XHR PATCHED] \${this._url}\`); 48 | } catch (e) { 49 | // ignore non-JSON 50 | } 51 | } 52 | }); 53 | return realXHROpen.apply(this, arguments); 54 | }; 55 | 56 | // -------- Hook fetch -------- 57 | const realFetch = window.fetch; 58 | window.fetch = async function(resource, init) { 59 | const response = await realFetch(resource, init); 60 | 61 | if (typeof resource === 'string' && resource.includes('/i/api/graphql/')) { 62 | try { 63 | const cloned = response.clone(); 64 | const data = await cloned.json(); 65 | 66 | recursiveClean(data); 67 | 68 | const patchedBody = JSON.stringify(data); 69 | return new Response(patchedBody, { 70 | status: response.status, 71 | statusText: response.statusText, 72 | headers: response.headers, 73 | }); 74 | } catch (e) { 75 | return response; 76 | } 77 | } else { 78 | return response; 79 | } 80 | }; 81 | })(); 82 | `; 83 | 84 | // Inject into page context via Blob URL (CSP-safe) 85 | const blob = new Blob([pageCode], { type: "application/javascript" }); 86 | const url = URL.createObjectURL(blob); 87 | const script = document.createElement('script'); 88 | script.src = url; 89 | document.documentElement.appendChild(script); 90 | script.remove(); 91 | })(); 92 | --------------------------------------------------------------------------------