├── .gitignore ├── assets ├── 16.png ├── 32.png ├── 48.png ├── 256.png ├── 512.png ├── icon.png ├── example.png ├── intro-1.png ├── intro-2.png ├── intro-3.png └── intro-4.png ├── options ├── icon.png ├── options.html ├── options.js └── options.css ├── manifest.v2.json ├── manifest.json ├── LICENSE ├── update_version_and_tag.ps1 ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── README.md ├── content.js └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .release/ -------------------------------------------------------------------------------- /assets/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/16.png -------------------------------------------------------------------------------- /assets/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/32.png -------------------------------------------------------------------------------- /assets/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/48.png -------------------------------------------------------------------------------- /assets/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/256.png -------------------------------------------------------------------------------- /assets/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/512.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/example.png -------------------------------------------------------------------------------- /assets/intro-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-1.png -------------------------------------------------------------------------------- /assets/intro-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-2.png -------------------------------------------------------------------------------- /assets/intro-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-3.png -------------------------------------------------------------------------------- /assets/intro-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/assets/intro-4.png -------------------------------------------------------------------------------- /options/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wseagar/eight-dollars/HEAD/options/icon.png -------------------------------------------------------------------------------- /manifest.v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Eight Dollars", 4 | "version": "2.0", 5 | "description": "Eight Dollars can help you tell the difference between actual verified accounts and twitter blue users.", 6 | "icons": { 7 | "16": "./assets/16.png", 8 | "32": "./assets/32.png", 9 | "48": "./assets/48.png", 10 | "256": "./assets/256.png", 11 | "512": "./assets/512.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["https://twitter.com/*", "https://mobile.twitter.com/*", "https://x.com/*", "https://mobile.x.com/*"], 16 | "js": ["content.js"] 17 | } 18 | ], 19 | "web_accessible_resources": ["script.js", "data/verified.txt"], 20 | "permissions": ["storage"], 21 | "browser_action": { 22 | "default_icon": "./assets/32.png", 23 | "default_title": "Eight-Dollars", 24 | "default_popup": "options/options.html" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Eight Dollars", 4 | "version": "2.0", 5 | "description": "Eight Dollars can help you tell the difference between actual verified accounts and twitter blue users.", 6 | "icons": { 7 | "16": "./assets/16.png", 8 | "32": "./assets/32.png", 9 | "48": "./assets/48.png", 10 | "256": "./assets/256.png", 11 | "512": "./assets/512.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["https://twitter.com/*", "https://mobile.twitter.com/*", "https://x.com/*", "https://mobile.x.com/*"], 16 | "js": ["content.js"] 17 | } 18 | ], 19 | "web_accessible_resources": [ 20 | { 21 | "resources": ["script.js", "data/verified.txt"], 22 | "matches": ["https://twitter.com/*", "https://mobile.twitter.com/*", "https://x.com/*", "https://mobile.x.com/*"] 23 | } 24 | ], 25 | "action": { 26 | "default_icon": { 27 | "16": "./assets/16.png", 28 | "32": "./assets/32.png" 29 | }, 30 | "default_title": "Eight Dollars - Options", 31 | "default_popup": "options/options.html" 32 | }, 33 | "permissions": ["storage"] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 William Seagar & Walter Lim 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 | -------------------------------------------------------------------------------- /update_version_and_tag.ps1: -------------------------------------------------------------------------------- 1 | # Prompt for version number 2 | $versionNumber = Read-Host -Prompt "Enter the version number" 3 | 4 | # Update version in manifest.json 5 | $manifestPath = "manifest.json" 6 | $manifest = Get-Content -Path $manifestPath | ConvertFrom-Json 7 | $manifest.version = $versionNumber 8 | $manifest | ConvertTo-Json -Depth 100 | Set-Content -Path $manifestPath 9 | 10 | # Check if Git is installed 11 | try { 12 | git --version | Out-Null 13 | } catch { 14 | Write-Host "Git is not installed or not in the PATH. Please install Git and try again." 15 | exit 1 16 | } 17 | 18 | # Check if the working directory is a Git repository 19 | try { 20 | git rev-parse --is-inside-work-tree | Out-Null 21 | } catch { 22 | Write-Host "The current directory is not a Git repository. Please initialize a Git repository and try again." 23 | exit 1 24 | } 25 | 26 | # Add all files to the Git staging area 27 | git add -A 28 | 29 | # Create a commit with the release number as the message 30 | $commitMessage = "Release v$versionNumber" 31 | git commit -m $commitMessage 32 | 33 | # Create a tag with the release number 34 | $tagName = "v$versionNumber" 35 | git tag $tagName 36 | 37 | Write-Host "Git commit and tag created successfully for the current release number." -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: noway, wseagar 7 | 8 | --- 9 | 10 | **IMPORTANT PLEASE READ (REMOVE THIS WHEN SUBMITTING A REPORT)** 11 | 12 | Before reporting a bug please make sure you are using the latest browser extension from the appropriate extension store. 13 | 14 | Extensions automatically update but may require a browser restart. Make sure that the issue persists after restarting your browser. 15 | 16 | **Info (please complete the following information):** 17 | - Browser [e.g. chrome, firefox] 18 | - Platform [e.g. Windows, Mac, Linux] 19 | - Extension Version [e.g. 1.3] 20 | - Location [e.g. US] (Twitter can roll out features in different regions at different times so this info is important to us) 21 | - Twitter Language (Include if using a non-English language) 22 | 23 | 24 | **What happened** 25 | Steps to reproduce the behavior: 26 | 1. Go to '...' 27 | 2. Click on '....' 28 | 3. Scroll down to '....' 29 | 4. See error 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Expected behavior** 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eight-dollars 2 | 3 | eight-dollars can help you tell the difference between actual verified accounts and twitter blue users. Just install the extension and see the difference. 4 | 5 | Maintained by [Will Seagar](https://twitter.com/willseagar), [Walter Lim](https://twitter.com/iWaltzAround), and [Ilia Sidorenko](https://twitter.com/noway421). 6 | 7 | Found this useful? [Buy us a $8 coffee here](https://www.buymeacoffee.com/eightdollars). 8 | 9 | ![Some tweets](./assets/example.png) 10 | 11 | 12 | ## Installation Instructions 13 | 14 | ### How to install on Chrome/Brave 15 | 16 | [Download it on the Chrome Web Store](https://chrome.google.com/webstore/detail/eight-dollars/fjbponfbognnefnmbffcfllkibbbobki) 17 | 18 | ### How to install on Firefox 19 | 20 | [Download it on the Firefox Browser Extensions page](https://addons.mozilla.org/en-US/firefox/addon/eightdollars/) 21 | 22 | ### How to install on Edge 23 | 24 | [Download it on the Microsoft Edge Add-ons page](https://microsoftedge.microsoft.com/addons/detail/eight-dollars/ehfacgbckjlegnlledgpkmkfbemhkknh) 25 | 26 | ### How to install on Opera 27 | 28 | 1. [Vist the Chrome Web Store page](https://chrome.google.com/webstore/detail/eight-dollars/fjbponfbognnefnmbffcfllkibbbobki) 29 | 2. Click "Add to Opera" 30 | 3. Click "Install" 31 | 32 | ### How to install on Safari 33 | 34 | Coming soon 35 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = { 2 | memeMode: false, 3 | textEnabled: true, 4 | removeBlueVerification: false, 5 | textOptions: { 6 | verifiedLabel: "Verified", 7 | twitterBlueLabel: "Paid", 8 | enableBorder: true, 9 | }, 10 | }; 11 | 12 | async function loadVerifiedUsers() { 13 | const url = chrome.runtime.getURL("data/verified.txt"); 14 | const response = await fetch(url); 15 | const text = await response.text(); 16 | return text.split("\n"); 17 | } 18 | 19 | function createSettingsDomNode(items) { 20 | const settingsDomNode = document.createElement("div"); 21 | settingsDomNode.id = "eight-dollars-settings"; 22 | settingsDomNode.style.display = "none"; 23 | settingsDomNode.innerText = JSON.stringify(items); 24 | document.body.appendChild(settingsDomNode); 25 | } 26 | 27 | function injectScript() { 28 | const s = document.createElement("script", { id: "eight-dollars" }); 29 | s.src = chrome.runtime.getURL("script.js"); 30 | s.onload = function () { 31 | this.remove(); 32 | }; 33 | document.head.appendChild(s); 34 | } 35 | 36 | function injectVerifiedUsers(verifiedUsers) { 37 | const verifiedUsersDomNode = document.createElement("div"); 38 | verifiedUsersDomNode.id = "eight-dollars-verified-users"; 39 | verifiedUsersDomNode.style.display = "none"; 40 | verifiedUsersDomNode.innerText = JSON.stringify(verifiedUsers); 41 | document.body.appendChild(verifiedUsersDomNode); 42 | } 43 | 44 | async function main() { 45 | const verifiedUsers = await loadVerifiedUsers(); 46 | 47 | if (typeof chrome !== "undefined" && chrome.storage) { 48 | chrome.storage.local.get(defaultConfig, function (items) { 49 | createSettingsDomNode(items); 50 | injectScript(); 51 | injectVerifiedUsers(verifiedUsers); 52 | }); 53 | } else { 54 | createSettingsDomNode(defaultConfig); 55 | injectScript(); 56 | injectVerifiedUsers(verifiedUsers); 57 | } 58 | } 59 | 60 | main(); 61 | -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eight Dollars Options 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Settings

15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 | 31 | 35 |
36 |
37 | 38 | 42 |
43 |
44 | 45 | 49 |
50 |
51 | 52 | 56 |
57 |
58 |
59 | Options Saved. Refresh your page to see changes. 60 |
61 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /options/options.js: -------------------------------------------------------------------------------- 1 | function onTextEnabledChange() { 2 | const textEnabled = document.getElementById("textEnabled").checked; 3 | // hide the other text options if text is disabled 4 | const elements = document.getElementsByClassName("text-option"); 5 | for (const elm of elements) { 6 | elm.style.display = textEnabled ? "block" : "none"; 7 | } 8 | } 9 | 10 | function saveOptions() { 11 | const memeMode = document.getElementById("memeMode").checked; 12 | const textEnabled = document.getElementById("textEnabled").checked; 13 | const textVerifiedLabel = document.getElementById("textVerifiedLabel").value; 14 | const twitterBlueVerifiedLabel = document.getElementById( 15 | "textTwitterBlueLabel" 16 | ).value; 17 | const textEnableBorder = document.getElementById("textEnableBorder").checked; 18 | const removeBlueVerification = document.getElementById( 19 | "removeBlueVerification" 20 | ).checked; 21 | chrome.storage.local.set( 22 | { 23 | memeMode, 24 | textEnabled, 25 | removeBlueVerification, 26 | textOptions: { 27 | verifiedLabel: textEnabled ? textVerifiedLabel : "", 28 | twitterBlueLabel: textEnabled ? twitterBlueVerifiedLabel : "", 29 | enableBorder: textEnabled ? textEnableBorder : true, 30 | }, 31 | }, 32 | function () { 33 | const status = document.getElementById("status"); 34 | status.style.display = "block"; 35 | setTimeout(function () { 36 | status.style.display = "none"; 37 | }, 1500); 38 | } 39 | ); 40 | } 41 | function closeOptions() { 42 | window.close(); 43 | } 44 | function restoreOptions() { 45 | chrome.storage.local.get( 46 | { 47 | memeMode: false, 48 | textEnabled: true, 49 | removeBlueVerification: false, 50 | textOptions: { 51 | verifiedLabel: "Verified", 52 | twitterBlueLabel: "Paid", 53 | enableBorder: true, 54 | }, 55 | }, 56 | function (items) { 57 | document.getElementById("memeMode").checked = items.memeMode; 58 | document.getElementById("textEnabled").checked = items.textEnabled; 59 | document.getElementById("textVerifiedLabel").value = 60 | items.textOptions.verifiedLabel; 61 | document.getElementById("textTwitterBlueLabel").value = 62 | items.textOptions.twitterBlueLabel; 63 | document.getElementById("textEnableBorder").checked = 64 | items.textOptions.enableBorder; 65 | document.getElementById("removeBlueVerification").checked = 66 | items.removeBlueVerification; 67 | onTextEnabledChange(); 68 | } 69 | ); 70 | } 71 | 72 | document.addEventListener("DOMContentLoaded", function () { 73 | restoreOptions(); 74 | document 75 | .getElementById("textEnabled") 76 | .addEventListener("change", onTextEnabledChange); 77 | document.getElementById("save").addEventListener("click", saveOptions); 78 | document.getElementById("close").addEventListener("click", closeOptions); 79 | }); 80 | -------------------------------------------------------------------------------- /options/options.css: -------------------------------------------------------------------------------- 1 | input { 2 | caret-color: #fff; 3 | } 4 | 5 | body { 6 | 7 | 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 9 | padding: 0; 10 | margin: 0; 11 | border: 1px solid #2f3336; 12 | background-color: black; 13 | ; 14 | 15 | } 16 | 17 | .savecontainer { 18 | box-sizing: border-box; 19 | padding: 1rem 1rem 1rem 1rem; 20 | border-top: 1px solid #2f3336; 21 | } 22 | 23 | .header { 24 | display: grid; 25 | box-sizing: border-box; 26 | grid-template-columns: 1fr 80px; 27 | justify-content: space-between; 28 | align-items: center; 29 | width: 100%; 30 | border-bottom: 1px solid #2f3336; 31 | padding: 1rem; 32 | } 33 | 34 | .header button { 35 | margin: 0; 36 | border: 1px solid #fff; 37 | background: none; 38 | } 39 | 40 | .header button:hover { 41 | margin: 0; 42 | background: #fff; 43 | color: black; 44 | } 45 | 46 | .header h1 { 47 | color: #fff; 48 | text-align: left; 49 | margin: 0; 50 | 51 | } 52 | 53 | p { 54 | color: #fff; 55 | font-size: 1.2rem; 56 | 57 | } 58 | 59 | .container { 60 | display: flex; 61 | flex-direction: column; 62 | align-items: center; 63 | text-align: center; 64 | justify-content: center; 65 | place-items: center; 66 | overflow: hidden; 67 | position: relative; 68 | border-radius: 20px; 69 | box-sizing: border-box; 70 | background: #000; 71 | width: 40rem; 72 | } 73 | 74 | .brand-logo { 75 | height: 32px; 76 | width: 32px; 77 | background: url("icon.png"); 78 | background-repeat: no-repeat; 79 | background-size: contain; 80 | margin: auto; 81 | border-radius: 50%; 82 | box-sizing: border-box; 83 | } 84 | 85 | #status { 86 | font-weight: 600; 87 | padding: 0.5rem; 88 | margin-bottom: 1rem; 89 | border: 1px solid white; 90 | border-radius: 16px; 91 | color: #fff; 92 | text-align: center; 93 | letter-spacing: 1px; 94 | display: none; 95 | } 96 | 97 | label, 98 | input, 99 | button { 100 | width: 100%; 101 | padding: 0; 102 | border: none; 103 | outline: none; 104 | box-sizing: border-box; 105 | font-size: 0.8rem; 106 | } 107 | 108 | label { 109 | margin-bottom: 3px; 110 | color: #fff; 111 | padding-bottom: 0.25rem; 112 | } 113 | 114 | 115 | input::placeholder { 116 | color: #888; 117 | } 118 | 119 | input { 120 | background: #000; 121 | padding: 1.5rem; 122 | padding-left: 20px; 123 | height: 30px; 124 | border-radius: 4px; 125 | border: 1px solid #2f3336; 126 | color: #fff; 127 | font-size: 1rem; 128 | } 129 | 130 | input[type="checkbox"] { 131 | box-shadow: none; 132 | height: 24px; 133 | } 134 | 135 | input[type="text"]:focus { 136 | border-color: #1da1f2; 137 | } 138 | 139 | .text-option { 140 | flex: 1; 141 | } 142 | 143 | .text-option label { 144 | font-size: 0.8rem; 145 | } 146 | 147 | .inputs { 148 | text-align: left; 149 | width: 100%; 150 | } 151 | 152 | .option {} 153 | 154 | .options { 155 | width: 100%; 156 | } 157 | 158 | .space { 159 | padding-top: 0.5rem; 160 | } 161 | 162 | .option.checkboxes { 163 | display: flex !important; 164 | align-content: center; 165 | border-top: 1px solid #2f3336; 166 | padding: 0.75rem 1rem; 167 | gap: 0.5rem; 168 | } 169 | 170 | .option.checkboxes label { 171 | margin: 0; 172 | padding: 0; 173 | margin-top: 0.05rem; 174 | } 175 | 176 | .option.checkboxes input { 177 | width: 2rem !important; 178 | } 179 | 180 | button { 181 | color: white; 182 | background: #1da1f2; 183 | height: 40px; 184 | border-radius: 20px; 185 | cursor: pointer; 186 | font-weight: 900; 187 | transition: 0.2s; 188 | } 189 | 190 | button:hover { 191 | box-shadow: none; 192 | } 193 | 194 | 195 | 196 | .inputs-split { 197 | display: flex; 198 | flex-direction: row; 199 | gap: 1rem; 200 | width: 100%; 201 | padding: 0.5rem 1rem; 202 | box-sizing: border-box; 203 | } -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const DEBUG = false; 2 | 3 | const config = document.getElementById("eight-dollars-settings"); 4 | const data = JSON.parse(config.innerText); 5 | 6 | const verifiedUsers = document.getElementById("eight-dollars-verified-users"); 7 | 8 | const verifiedUsersData = JSON.parse(verifiedUsers.innerText); 9 | const parsedVerifiedUsersData = verifiedUsersData.map((user) => 10 | user.toLowerCase().replace("\r", "") 11 | ); 12 | const VERIFIED_USERS = new Set(parsedVerifiedUsersData); 13 | console.log(VERIFIED_USERS); 14 | 15 | const MEME_MODE = data.memeMode; 16 | const TEXT_ENABLED = data.textEnabled; 17 | const REMOVE_TWITTER_BLUE_VERIFICATION = data.removeBlueVerification; 18 | const TEXT_VERIFIED_LABEL = data.textOptions?.verifiedLabel || ""; 19 | const TEXT_TWITTER_BLUE_LABEL = data.textOptions?.twitterBlueLabel || ""; 20 | const TEXT_ENABLE_BORDER = data.textOptions?.enableBorder ?? true; 21 | 22 | const VERIFIED_ACCOUNT_ARIA_LABEL_I18N = { 23 | "ar-x-fm": "حساب موَثَّق", 24 | ar: "حساب موَثَّق", 25 | bg: "Потвърден профил", 26 | bn: "যাচাইকৃত অ্যাকাউন্ট", 27 | ca: "Compte verificat", 28 | cs: "Ověřený účet", 29 | da: "Verificeret konto", 30 | de: "Verifizierter Account", 31 | el: "Επαληθευμένος λογαριασμός", 32 | "en-gb": "Verified account", 33 | en: "Verified account", 34 | es: "Cuenta verificada", 35 | eu: "Egiaztatutako kontua", 36 | fa: "حساب تاییدشده", 37 | fi: "Varmennettu tili", 38 | fil: "Beripikadong account", 39 | fr: "Compte certifié", 40 | ga: "Cuntas deimhnithe", 41 | gl: "Conta verificada", 42 | gu: "ચકાસાયેલું એકાઉન્ટ", 43 | he: "חשבון מאומת", 44 | hi: "सत्यापित खाता", 45 | hr: "Potvrđeni račun", 46 | hu: "Hitelesített felhasználó", 47 | id: "Akun terverifikasi", 48 | it: "Account verificato", 49 | ja: "認証済みアカウント", 50 | kn: "ಪರಿಶೀಲಿಸಿದ ಖಾತೆ", 51 | ko: "인증된 계정", 52 | mr: "खाते सत्यापित केले", 53 | ms: "Akaun Disahkan", 54 | nb: "Verifisert konto", 55 | nl: "Geverifieerd account", 56 | pl: "Zweryfikowane konto", 57 | pt: "Conta verificada", 58 | ro: "Cont verificat", 59 | ru: "Подлинная учетная запись", 60 | sk: "Overený účet", 61 | sr: "Потврђен налог", 62 | sv: "Verifierat konto", 63 | ta: "சரிபார்க்கப்பட்ட கணக்கு", 64 | th: "บัญชีที่ยืนยันแล้ว", 65 | tr: "Onaylanmış hesap", 66 | uk: "Підтверджений профіль", 67 | ur: "تصدیق شدہ اکاؤنٹ", 68 | vi: "Tài khoản đã xác nhận", 69 | "zh-hant": "已認證的帳戶", 70 | zh: "认证账号", 71 | }; 72 | const PROVIDES_DETAILS_ARIA_LABEL_I18N = { 73 | "ar-x-fm": "لتوفير تفاصيل حول الحسابات الموثّقة.", 74 | ar: "لتوفير تفاصيل حول الحسابات الموثّقة.", 75 | bg: "Дава подробности за потвърдените профили.", 76 | bn: "যাচাই করা অ্যাকাউন্ট সম্পর্কে বিশদ বিবরণ দিন।", 77 | ca: "Proporciona informació sobre els comptes verificats.", 78 | cs: "Poskytuje podrobnosti o ověřených účtech.", 79 | da: "Giver oplysninger om verificerede konti.", 80 | de: "Gibt Details über verifizierte Accounts an.", 81 | el: "Παρέχει λεπτομέρειες σχετικά με τους επαληθευμένους λογαριασμούς.", 82 | "en-gb": "Provides details about verified accounts.", 83 | en: "Provides details about verified accounts.", 84 | es: "Proporciona detalles sobre las cuentas verificadas.", 85 | eu: "Egiaztatutako kontuei buruzko xehetasunak ematen ditu.", 86 | fa: "جزئیاتی درباره حساب‌های کاربری تأییدشده ارائه می‌دهد.", 87 | fi: "Tarkempia tietoja varmennetuista tileistä.", 88 | fil: "Nagbibigay ng mga detalye tungkol sa mga beripikadong account.", 89 | fr: "Donne des détails sur les comptes certifiés.", 90 | ga: "Soláthraítear sonraí maidir le cuntais dheimhnithe.", 91 | gl: "Fornece detalles sobre contas verificadas.", 92 | gu: "ચકાસાયેલ એકાઉન્ટ વિષે વિગતો આપે છે.", 93 | he: "מידע לגבי חשבונות מאומתים.", 94 | hi: "सत्यापित खातों के विवरण प्रदान करें.", 95 | hr: "Daje podatke o potvrđenim računima.", 96 | hu: "További részletek az ellenőrzött felhasználói fiókokkal kapcsolatban.", 97 | id: "Memberikan detail tentang akun terverifikasi.", 98 | it: "Fornisce dettagli sugli account verificati.", 99 | ja: "認証済みアカウントについての詳細が表示されます。", 100 | kn: "ಪರಿಶೀಲಿಸಿದ ಖಾತೆಗಳ ಬಗ್ಗೆ ವಿವರಗಳನ್ನು ಒದಗಿಸುತ್ತದೆ.", 101 | ko: "인증된 계정에 대한 세부 정보를 제공합니다.", 102 | mr: "सत्यापित खात्यांविषयी तपशील दिला जातो.", 103 | ms: "Menyediakan butiran tentang akaun disahkan.", 104 | nb: "Gir detaljert informasjon om verifiserte kontoer.", 105 | nl: "Verstrekt informatie over geverifieerde accounts.", 106 | pl: "Więcej informacji na temat weryfikacji kont.", 107 | pt: "Dá detalhes sobre contas verificadas.", 108 | ro: "Oferă detalii despre conturile verificate.", 109 | ru: "Объясняет, что такое подлинные учетные записи.", 110 | sk: "Poskytne podrobnosti o overených účtoch.", 111 | sr: "Наводи детаље о потврђеним налозима.", 112 | sv: "Ger information om verifierade konton.", 113 | ta: "சரிபார்க்கப்பட்ட கணக்குகள் பற்றிய விவரங்களை வழங்குகிறது.", 114 | th: "ระบุรายละเอียดเกี่ยวกับบัญชีที่ยืนยันแล้ว", 115 | tr: "Onaylanmış hesaplar hakkında ayrıntılı bilgi verir.", 116 | uk: "Надає відомості про підтверджені профілі.", 117 | ur: "تصدیق شدہ اکاؤنٹس کے متعلق تفصیلات فراہم کریں۔", 118 | vi: "Cung cấp thông tin chi tiết về tài khoản đã xác minh.", 119 | "zh-hant": "提供已認證帳戶的詳細資料。", 120 | zh: "提供已验证账号的详细信息。", 121 | }; 122 | 123 | const lang = document.documentElement.lang.toLowerCase(); 124 | const VERIFIED_ACCOUNT_ARIA_LABEL = 125 | VERIFIED_ACCOUNT_ARIA_LABEL_I18N[lang] || VERIFIED_ACCOUNT_ARIA_LABEL_I18N.en; 126 | const PROVIDES_DETAILS_ARIA_LABEL = 127 | PROVIDES_DETAILS_ARIA_LABEL_I18N[lang] || PROVIDES_DETAILS_ARIA_LABEL_I18N.en; 128 | 129 | const COMIC_SANS_BLUE_DOLLAR_SVG = ( 130 | isAriaLabel, 131 | className 132 | ) => ` 137 | 138 | 139 | `; 140 | 141 | const REGULAR_BLUE_DOLLAR_SVG = ( 142 | isAriaLabel, 143 | className 144 | ) => ` 149 | 150 | 151 | 152 | `; 153 | 154 | const REGULAR_BLUE_CHECK_SVG = (isAriaLabel, className) => 155 | ``; 160 | 161 | function getOriginalClasses(elm) { 162 | if (elm.dataset.eightDollarsOriginalClasses) { 163 | return elm.dataset.eightDollarsOriginalClasses; 164 | } 165 | return [...elm.classList].join(" "); 166 | } 167 | 168 | function changeVerified(prependHTML, elm, isSmall, isIndeterminate) { 169 | if (elm.dataset.eightDollarsStatus === "verified") { 170 | // already replaced this element 171 | return; 172 | } 173 | 174 | // hide if believe there's bad data 175 | if (isIndeterminate) { 176 | elm.style.display = "none"; 177 | elm.setAttribute("data-eight-dollars-status", "verified"); 178 | return; 179 | } 180 | 181 | const small = REGULAR_BLUE_CHECK_SVG(true, getOriginalClasses(elm)); 182 | const smallInnerElement = REGULAR_BLUE_CHECK_SVG( 183 | false, 184 | getOriginalClasses(elm) 185 | ); 186 | const big = ` 187 | 196 | ${smallInnerElement} 197 |

${TEXT_VERIFIED_LABEL}

198 |
`; 199 | try { 200 | if (prependHTML !== "") { 201 | // Ideally, we wouldn't mutate the parent element, because those 2 styles won't be managed by us further on. 202 | // That is, if the `aria-label`-selected element changes, the parent styles won't be properly updated. 203 | // This approach is tolerable, because it's unlikely that a `aria-label`-selected element changes from a 204 | // "React text node(s) + React element node" sibling configuration to a "single React element node" sibling configuration. 205 | elm.style.display = "inline-flex"; 206 | elm.style.alignItems = "center"; 207 | } 208 | if (isSmall || !TEXT_ENABLED) { 209 | elm.innerHTML = `${prependHTML}${small}`; 210 | } else { 211 | elm.innerHTML = `${prependHTML}${big}`; 212 | } 213 | } catch (e) { 214 | console.error("error changing verified", e); 215 | } 216 | } 217 | 218 | function changeBlueVerified(prependHTML, elm, isSmall) { 219 | if (elm.dataset.eightDollarsStatus === "blueVerified") { 220 | // already replaced this element 221 | return; 222 | } 223 | 224 | if (REMOVE_TWITTER_BLUE_VERIFICATION) { 225 | elm.style.display = "none"; 226 | elm.setAttribute("data-eight-dollars-status", "blueVerified"); 227 | return; 228 | } 229 | 230 | const small = MEME_MODE 231 | ? `${COMIC_SANS_BLUE_DOLLAR_SVG(true, getOriginalClasses(elm))}` 232 | : `${REGULAR_BLUE_DOLLAR_SVG(true, getOriginalClasses(elm))}`; 233 | const smallInnerElement = MEME_MODE 234 | ? `${COMIC_SANS_BLUE_DOLLAR_SVG(false, getOriginalClasses(elm))}` 235 | : `${REGULAR_BLUE_DOLLAR_SVG(false, getOriginalClasses(elm))}`; 236 | const big = ` 237 | 246 | ${smallInnerElement} 247 |

${TEXT_TWITTER_BLUE_LABEL}

248 |
`; 249 | try { 250 | if (prependHTML !== "") { 251 | // Ideally, we wouldn't mutate the parent element, because those 2 styles won't be managed by us further on. 252 | // That is, if the `aria-label`-selected element changes, the parent styles won't be properly updated. 253 | // This approach is tolerable, because it's unlikely that a `aria-label`-selected element changes from a 254 | // "React text node(s) + React element node" sibling configuration to a "single React element node" sibling configuration. 255 | elm.style.display = "inline-flex"; 256 | elm.style.alignItems = "center"; 257 | } 258 | if (isSmall || !TEXT_ENABLED) { 259 | elm.innerHTML = `${prependHTML}${small}`; 260 | } else { 261 | elm.innerHTML = `${prependHTML}${big}`; 262 | } 263 | } catch (e) { 264 | console.error("error changing blue verified", e); 265 | } 266 | } 267 | 268 | function querySelectorAllIncludingMe(node, selector) { 269 | if (node.matches(selector)) { 270 | return [node]; 271 | } 272 | return [...node.querySelectorAll(selector)]; 273 | } 274 | 275 | const trackingTweets = new Set(); 276 | const trackingHovercards = new Set(); 277 | const trackingUsercells = new Set(); 278 | const trackingConversation = new Set(); 279 | const trackingDmDrawHeader = new Set(); 280 | const trackingUserName = new Set(); 281 | const trackingHeadings = new Set(); 282 | 283 | function collectAndTrackElements(node) { 284 | const tweets = querySelectorAllIncludingMe(node, '[data-testid="tweet"]'); 285 | for (const tweet of tweets) { 286 | trackingTweets.add(tweet); 287 | } 288 | const hovercards = querySelectorAllIncludingMe( 289 | node, 290 | '[data-testid="HoverCard"]' 291 | ); 292 | for (const hovercard of hovercards) { 293 | trackingHovercards.add(hovercard); 294 | } 295 | const usercells = querySelectorAllIncludingMe( 296 | node, 297 | '[data-testid="UserCell"]' 298 | ); 299 | for (const usercell of usercells) { 300 | trackingUsercells.add(usercell); 301 | } 302 | const conversations = querySelectorAllIncludingMe( 303 | node, 304 | '[data-testid="conversation"]' 305 | ); 306 | for (const conversation of conversations) { 307 | trackingConversation.add(conversation); 308 | } 309 | const dmDrawHeader = querySelectorAllIncludingMe( 310 | node, 311 | '[data-testid="DMDrawerHeader"]' 312 | ); 313 | for (const dm of dmDrawHeader) { 314 | trackingDmDrawHeader.add(dm); 315 | } 316 | const userName = querySelectorAllIncludingMe( 317 | node, 318 | '[data-testid="UserName"]' 319 | ); 320 | for (const name of userName) { 321 | trackingUserName.add(name); 322 | } 323 | const headings = querySelectorAllIncludingMe( 324 | node, 325 | '[data-testid="primaryColumn"] [role="heading"][aria-level="2"][dir="ltr"]' 326 | ); 327 | for (const heading of headings) { 328 | trackingHeadings.add(heading); 329 | } 330 | } 331 | 332 | function handleProfileModification(containerElement, isBig) { 333 | const ltr = containerElement.querySelectorAll('[dir="ltr"]'); 334 | 335 | const checkmarkContainer = ltr[0]; 336 | const spans = checkmarkContainer.querySelectorAll("span"); 337 | const checkmarkSpan = spans[spans.length - 1]; 338 | const protectd = checkmarkSpan.querySelectorAll('[aria-label="Protected account"]')[0] 339 | 340 | if (!checkmarkSpan || protectd) { 341 | console.error("no checkmark span found"); 342 | return; 343 | } 344 | 345 | let hasVerifiedIcon = checkmarkSpan.children.length > 0; 346 | 347 | const handleContainer = ltr[1]; 348 | const handle = handleContainer.innerText; 349 | if (!handle) { 350 | return; 351 | } 352 | const parsedHandle = handle.replace("@", "").toLowerCase(); 353 | const isLegacyVerifed = VERIFIED_USERS.has(parsedHandle); 354 | 355 | if (isLegacyVerifed) { 356 | changeVerified("", checkmarkSpan, isBig); 357 | } else if (hasVerifiedIcon) { 358 | changeBlueVerified("", checkmarkSpan, isBig); 359 | } 360 | 361 | if (DEBUG) { 362 | if (isLegacyVerifed) { 363 | containerElement.style = "border: 1px solid green;"; 364 | } else if (hasVerifiedIcon) { 365 | containerElement.style = "border: 1px solid red;"; 366 | } 367 | 368 | // add a plain text node to the tweet 369 | containerElement.appendChild( 370 | document.createTextNode( 371 | `isLegacyVerified: ${isLegacyVerifed.toString()}\n verifiedIcon: ${hasVerifiedIcon.toString()}\n` 372 | ) 373 | ); 374 | } 375 | } 376 | 377 | function waitForElm(selector) { 378 | return new Promise((resolve) => { 379 | if (document.querySelector(selector)) { 380 | return resolve(document.querySelector(selector)); 381 | } 382 | 383 | const observer = new MutationObserver((mutations) => { 384 | if (document.querySelector(selector)) { 385 | resolve(document.querySelector(selector)); 386 | observer.disconnect(); 387 | } 388 | }); 389 | 390 | observer.observe(document.body, { 391 | childList: true, 392 | subtree: true, 393 | }); 394 | }); 395 | } 396 | 397 | async function handleHeadingModification(containerElement, isBig) { 398 | const profile = await waitForElm('[data-testid="UserName"]'); 399 | if (!profile) { 400 | console.error("no profile found"); 401 | return; 402 | } 403 | const ltr = profile.querySelectorAll('[dir="ltr"]'); 404 | const handleContainer = ltr[1]; 405 | const handle = handleContainer.innerText; 406 | if (!handle) { 407 | return; 408 | } 409 | const parsedHandle = handle.replace("@", "").toLowerCase(); 410 | const isLegacyVerifed = VERIFIED_USERS.has(parsedHandle); 411 | 412 | const spans = containerElement.querySelectorAll("span"); 413 | const checkmarkSpan = spans[spans.length - 1]; 414 | const protectd = checkmarkSpan.querySelectorAll('[aria-label="Protected account"]')[0] 415 | 416 | if (!checkmarkSpan || protectd) { 417 | console.error("no checkmark span found"); 418 | return; 419 | } 420 | 421 | let hasVerifiedIcon = checkmarkSpan.children.length > 0; 422 | 423 | if (isLegacyVerifed) { 424 | changeVerified("", checkmarkSpan, isBig); 425 | } else if (hasVerifiedIcon) { 426 | changeBlueVerified("", checkmarkSpan, isBig); 427 | } 428 | 429 | if (DEBUG) { 430 | if (isLegacyVerifed) { 431 | containerElement.style = "border: 1px solid green;"; 432 | } else if (hasVerifiedIcon) { 433 | containerElement.style = "border: 1px solid red;"; 434 | } 435 | 436 | // add a plain text node to the tweet 437 | containerElement.appendChild( 438 | document.createTextNode( 439 | `isLegacyVerified: ${isLegacyVerifed.toString()}\n verifiedIcon: ${hasVerifiedIcon.toString()}\n` 440 | ) 441 | ); 442 | } 443 | } 444 | 445 | function handleModification( 446 | containerElement, 447 | checkmarkIndex, 448 | handleIndex, 449 | isBig 450 | ) { 451 | const ltr = containerElement.querySelectorAll('[dir="ltr"]'); 452 | const checkmarkContainer = ltr[checkmarkIndex]; 453 | const checkmarkSpan = checkmarkContainer.children?.[0]; 454 | const protectd = checkmarkSpan.querySelectorAll('[aria-label="Protected account"]')[0] 455 | 456 | if (!checkmarkSpan || protectd) { 457 | return; 458 | } 459 | 460 | let hasVerifiedIcon = checkmarkSpan.children.length > 0; 461 | 462 | const handleContainer = ltr[handleIndex]; 463 | const handle = handleContainer.innerText; 464 | if (!handle) { 465 | return; 466 | } 467 | const parsedHandle = handle.replace("@", "").toLowerCase(); 468 | const isLegacyVerifed = VERIFIED_USERS.has(parsedHandle); 469 | 470 | if (isLegacyVerifed) { 471 | changeVerified("", checkmarkSpan, isBig); 472 | } else if (hasVerifiedIcon) { 473 | changeBlueVerified("", checkmarkSpan, isBig); 474 | } 475 | 476 | if (DEBUG) { 477 | if (isLegacyVerifed) { 478 | containerElement.style = "border: 1px solid green;"; 479 | } else if (hasVerifiedIcon) { 480 | containerElement.style = "border: 1px solid red;"; 481 | } 482 | 483 | // add a plain text node to the tweet 484 | containerElement.appendChild( 485 | document.createTextNode( 486 | `isLegacyVerified: ${isLegacyVerifed.toString()}\n verifiedIcon: ${hasVerifiedIcon.toString()}\n` 487 | ) 488 | ); 489 | } 490 | } 491 | 492 | async function main() { 493 | const observer = new MutationObserver(function (mutations, observer) { 494 | try { 495 | for (const mutation of mutations) { 496 | if (mutation.type === "attributes") { 497 | collectAndTrackElements(mutation.target); 498 | } 499 | for (const node of mutation.addedNodes) { 500 | if (node.nodeType === 1) { 501 | collectAndTrackElements(node); 502 | } 503 | } 504 | } 505 | 506 | for (const tweet of trackingTweets) { 507 | if (!tweet.dataset.processed) { 508 | // includes subtweets 509 | const userNameContainer = tweet.querySelectorAll( 510 | '[data-testid="User-Name"]' 511 | ); 512 | for (const container of userNameContainer) { 513 | handleModification(container, 1, 2, false); 514 | } 515 | 516 | tweet.dataset.processed = true; 517 | } 518 | } 519 | for (const hovercard of trackingHovercards) { 520 | if (!hovercard.dataset.processed) { 521 | handleModification(hovercard, 2, 3, false); 522 | 523 | hovercard.dataset.processed = true; 524 | } 525 | } 526 | for (const usercell of trackingUsercells) { 527 | if (!usercell.dataset.processed) { 528 | handleModification(usercell, 1, 2, true); 529 | usercell.dataset.processed = true; 530 | } 531 | } 532 | for (const conversation of trackingConversation) { 533 | if (!conversation.dataset.processed) { 534 | handleModification(conversation, 1, 2, true); 535 | 536 | conversation.dataset.processed = true; 537 | } 538 | } 539 | for (const dm of trackingDmDrawHeader) { 540 | if (!dm.dataset.processed) { 541 | //handleModification(dm, 2, 2, true); 542 | 543 | dm.dataset.processed = true; 544 | } 545 | } 546 | for (const name of trackingUserName) { 547 | if (!name.dataset.processed) { 548 | handleProfileModification(name); 549 | 550 | name.dataset.processed = true; 551 | } 552 | } 553 | for (const heading of trackingHeadings) { 554 | if (!heading.dataset.processed) { 555 | handleHeadingModification(heading); 556 | 557 | heading.dataset.processed = true; 558 | } 559 | } 560 | } catch (error) { 561 | console.log("uncaught mutation error", error); 562 | } 563 | }); 564 | 565 | observer.observe(document, { 566 | childList: true, 567 | subtree: true, 568 | attributes: true, 569 | }); 570 | } 571 | 572 | main(); 573 | --------------------------------------------------------------------------------