├── README.MD ├── LICENSE └── script.js /README.MD: -------------------------------------------------------------------------------- 1 | ### Twitter Challenge 2 | See the challenge here: https://twitter.com/realGeorgeHotz/status/1595270867402956801 3 | 4 | To use this plugin: 5 | 1. Install [Tampermonkey for Chrome](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=en) 6 | 2. Create new script in it and copy the content from script.js to it 7 | 3. ??? 8 | 4. Profit! 9 | 10 | `hint: use arrows to switch bettween suggestions, enter to confirm` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yaroslav Nazarov 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 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Twitter Super Search Box 3 | // @namespace http://tampermonkey.net/ 4 | // @version 0.1 5 | // @description try to take over the world! 6 | // @author Yaroslav 7 | // @match https://twitter.com/* 8 | // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | const searchboxSelector = "input[data-testid=\"SearchBox_Search_Input\"]"; 13 | const typeAheadUrl = "https://twitter.com/i/api/1.1/search/typeahead.json"; 14 | const isFromSuggestRegex = /^(from:)([^ ]*)$/; 15 | const ifFromSearchRegex = /^(from:)([^ ]+) (.*)$/; 16 | let hideSuggestionStyleEl; 17 | let timeout; 18 | const typeAheadDebounceMs = 200; 19 | let selectedHandle = ''; 20 | let suggestions = []; // [{avatarUrl, name, twitterHandle, bio, isBlueTick}] 21 | 22 | const addPluginCSS = () => { 23 | const head = document.head || document.getElementsByTagName('head')[0]; 24 | const typeAheadCss = document.createElement('style'); 25 | head.appendChild(typeAheadCss); 26 | typeAheadCss.appendChild(document.createTextNode(` 27 | .pluginSuggestionContainer { 28 | width: 100%; 29 | display: flex; 30 | flex-direction:row; 31 | cursor:pointer; 32 | box-sizing: border-box; 33 | padding: 12px 16px; 34 | font-family: TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 35 | font-size: 15px; 36 | line-height: 20px; 37 | } 38 | .pluginSuggestionContainer:hover { 39 | background-color: rgb(247,249,249); 40 | } 41 | 42 | .pluginSuggestionContainer.text:hover{ 43 | cursor:default; 44 | background-color: transparent; 45 | } 46 | 47 | .pluginSuggestionContainer.selected { 48 | background-color: rgb(247,249,249); 49 | } 50 | 51 | .pluginAvatar { 52 | border-radius: 1000px; 53 | height: 56px; 54 | width: 56px; 55 | margin-right: 12px; 56 | } 57 | 58 | .pluginSuggestionName { 59 | color: rgb(15,20,25); 60 | font-weight: 700; 61 | } 62 | 63 | .pluginHandle { 64 | color: rgb(83,100,113); 65 | } 66 | 67 | .pluginBio { 68 | max-lines: 1; 69 | color: rgb(83,100,113); 70 | text-overflow: ellipsis; 71 | } 72 | `)); 73 | } 74 | 75 | // looking for SearchBox to appear/disappear 76 | const createObserver = (onSearchAdded, onSearchRemoved) => { 77 | return new MutationObserver(function(mutations_list) { 78 | mutations_list.forEach(function(mutation) { 79 | mutation.addedNodes.forEach(function(added_node) { 80 | if (added_node.querySelector) { 81 | const searchEl = added_node.querySelector(searchboxSelector); 82 | if (!!searchEl) { 83 | onSearchAdded(searchEl); 84 | }; 85 | } 86 | }); 87 | 88 | mutation.removedNodes.forEach(function(removed_node) { 89 | const searchEl = removed_node.querySelector(searchboxSelector); 90 | if (!!searchEl) { 91 | onSearchRemoved(searchEl); 92 | }; 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | const getCookie = (cname) => { 99 | let name = cname + "="; 100 | let decodedCookie = decodeURIComponent(document.cookie); 101 | let ca = decodedCookie.split(';'); 102 | for(let i = 0; i { 115 | const searchEl = document.querySelector(searchboxSelector); 116 | if (searchEl) { 117 | searchEl.focus(); 118 | searchEl.value = text; 119 | } 120 | } 121 | 122 | const getTypeAhead = (twitterHandle) => { 123 | return new Promise((resolve, reject) => { 124 | const requestUrl = new URL(typeAheadUrl); 125 | const csrfToken = getCookie("ct0"); 126 | const isLoggedIn = !!getCookie("twid"); 127 | const guestToken = getCookie("gt"); 128 | // constant in twitter js code 129 | const authorization = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; 130 | 131 | requestUrl.searchParams.set('include_ext_is_blue_verified', 1); 132 | requestUrl.searchParams.set('q', `@${twitterHandle}`); 133 | requestUrl.searchParams.set('src', 'search_box'); 134 | requestUrl.searchParams.set('result_type', 'users'); 135 | 136 | const xmlHttp = new XMLHttpRequest(); 137 | xmlHttp.open("GET", requestUrl.toString(), false); 138 | xmlHttp.setRequestHeader('x-csrf-token', csrfToken); 139 | xmlHttp.setRequestHeader('x-twitter-active-user', 'yes'); 140 | if(isLoggedIn){ 141 | xmlHttp.setRequestHeader('x-twitter-auth-type', 'OAuth2Session'); 142 | } else { 143 | xmlHttp.setRequestHeader('x-guest-token', guestToken); 144 | } 145 | xmlHttp.setRequestHeader('x-twitter-client-language', 'en'); 146 | xmlHttp.setRequestHeader('authorization', `Bearer ${authorization}`); 147 | 148 | xmlHttp.onload = (e) => { 149 | if (xmlHttp.readyState === 4) { 150 | if (xmlHttp.status === 200) { 151 | resolve(xmlHttp.responseText); 152 | } else { 153 | reject(xmlHttp.statusText); 154 | } 155 | } 156 | } 157 | 158 | xmlHttp.onerror = (e) => { 159 | reject(xmlHttp.statusTexT); 160 | } 161 | 162 | xmlHttp.send(null); 163 | }); 164 | } 165 | 166 | const removePrevSuggestions = () => { 167 | const prevSuggestions = document.querySelectorAll('.pluginSuggestionContainer'); 168 | prevSuggestions.forEach(el => el.remove()); 169 | } 170 | 171 | const showText = (text, onClick) => { 172 | removePrevSuggestions(); 173 | const suggestionsContainer = document.querySelector('div[id^=typeaheadDropdown-]'); 174 | const container = document.createElement('div'); 175 | container.classList.add("pluginSuggestionContainer"); 176 | if (onClick) { 177 | container.addEventListener('mousedown', onClick); 178 | } else { 179 | container.classList.add("text"); 180 | } 181 | 182 | const textEl = document.createElement('div'); 183 | container.classList.add('pluginTextEl'); 184 | const handleText = document.createTextNode(text); 185 | textEl.appendChild(handleText); 186 | container.appendChild(textEl); 187 | suggestionsContainer.appendChild(container); 188 | } 189 | 190 | const showPluginSuggestions = (suggestions) => { 191 | // removing all previous suggestions 192 | removePrevSuggestions(); 193 | 194 | const suggestionsContainer = document.querySelector('div[id^=typeaheadDropdown-]'); 195 | 196 | console.log('>>> selectedHandle', selectedHandle); 197 | 198 | suggestions.forEach(s => { 199 | const container = document.createElement('div'); 200 | container.classList.add("pluginSuggestionContainer"); 201 | if (s.twitterHandle === selectedHandle) { 202 | container.classList.add("selected"); 203 | } 204 | 205 | const avatar = document.createElement('img'); 206 | avatar.classList.add("pluginAvatar"); 207 | avatar.setAttribute("src", s.avatarUrl); 208 | container.appendChild(avatar); 209 | 210 | const textContainer = document.createElement('div'); 211 | textContainer.classList.add("pluginTextContainer"); 212 | 213 | const name = document.createElement('div'); 214 | name.classList.add('pluginSuggestionName'); 215 | const nameText = document.createTextNode(s.name); 216 | name.appendChild(nameText); 217 | textContainer.appendChild(name); 218 | 219 | const handle = document.createElement('div'); 220 | handle.classList.add('pluginHandle'); 221 | const handleText = document.createTextNode(s.twitterHandle); 222 | handle.appendChild(handleText); 223 | textContainer.appendChild(handle); 224 | 225 | const bio = document.createElement('div'); 226 | bio.classList.add('pluginBio'); 227 | const bioText = document.createTextNode(s.bio); 228 | bio.appendChild(bioText); 229 | textContainer.appendChild(bio); 230 | 231 | container.appendChild(textContainer); 232 | container.addEventListener("click", () => setInputText(`from:${s.twitterHandle} `)); 233 | 234 | suggestionsContainer.appendChild(container); 235 | }); 236 | } 237 | 238 | const hideNativeSuggestions = () => { 239 | if(!hideSuggestionStyleEl) { 240 | console.log('>>> hideNativeSuggestions'); 241 | const head = document.head || document.getElementsByTagName('head')[0]; 242 | hideSuggestionStyleEl = document.createElement('style'); 243 | head.appendChild(hideSuggestionStyleEl); 244 | hideSuggestionStyleEl.appendChild(document.createTextNode(` 245 | div[data-testid="typeaheadResult"] { 246 | display: none !important; 247 | }; 248 | `)); 249 | } 250 | } 251 | 252 | const showNativeSuggestions = () => { 253 | removePrevSuggestions(); 254 | if (hideSuggestionStyleEl) { 255 | console.log('>>> showNativeSuggestions');; 256 | hideSuggestionStyleEl.remove() 257 | hideSuggestionStyleEl = undefined; 258 | }; 259 | } 260 | 261 | const onKeyDown = (e) => { 262 | if (suggestions.length) { 263 | if (e.keyCode == '38') { // up 264 | let newIndex = suggestions.length - 1; 265 | const selectedIndex = suggestions.findIndex((s) => s.twitterHandle === selectedHandle); 266 | if (selectedIndex > 0) { 267 | newIndex = selectedIndex - 1; 268 | } 269 | selectedHandle = suggestions[newIndex].twitterHandle; 270 | showPluginSuggestions(suggestions); 271 | 272 | } else if (e.keyCode == '40') { // down 273 | let newIndex = 0; 274 | const selectedIndex = suggestions.findIndex((s) => s.twitterHandle === selectedHandle); 275 | if (selectedIndex < suggestions.length - 1) { 276 | newIndex = selectedIndex + 1; 277 | } 278 | selectedHandle = suggestions[newIndex].twitterHandle; 279 | showPluginSuggestions(suggestions); 280 | } else if (e.keyCode == '13' && selectedHandle) { //enter 281 | e.preventDefault(); 282 | e.stopPropagation(); 283 | setInputText(`from:${selectedHandle} `); 284 | selectedHandle = ''; 285 | suggestions = []; 286 | } 287 | } 288 | }; 289 | 290 | const onSearchChange = (event) => { 291 | const text = event.target.value; 292 | const isFromSuggest = isFromSuggestRegex.test(text); 293 | const isFromSearch = ifFromSearchRegex.test(text); 294 | 295 | if (isFromSuggest) { 296 | const match = text.match(isFromSuggestRegex); 297 | const twitterHandle = match[2]; 298 | if (twitterHandle === '') { 299 | showText('Keep typing a user name...') 300 | } else { 301 | 302 | if (timeout) { 303 | clearTimeout(timeout); 304 | }; 305 | timeout = setTimeout(() => { 306 | getTypeAhead(twitterHandle).then((resultsText) => { 307 | const results = JSON.parse(resultsText); 308 | 309 | if (!results.users.length) { 310 | showText('No users found') 311 | } else { 312 | 313 | suggestions = results.users.map((u) => { 314 | return { 315 | avatarUrl: u.profile_image_url_https, 316 | name: u.name, 317 | twitterHandle: '@'+u.screen_name, 318 | bio: u.result_context.display_string || '', 319 | isBlueTick: u.verified || u.ext_is_blue_verified, 320 | }}); 321 | 322 | showPluginSuggestions(suggestions); 323 | }; 324 | }); 325 | }, typeAheadDebounceMs); 326 | } 327 | } 328 | if (isFromSearch) { 329 | const match = text.match(ifFromSearchRegex); 330 | const twitterHandle = match[2]; 331 | const searchText = match[3]; 332 | suggestions = []; 333 | showText(`Search "${searchText}" in ${twitterHandle} tweets`, () => { 334 | // non-ideal, as it reloads the page. But simulating keydown events didn't work. 335 | const searchUrl = new URL('https://twitter.com/search'); 336 | searchUrl.searchParams.set('q', `from:${twitterHandle} ${searchText}`); 337 | searchUrl.searchParams.set('src', 'typed_query'); 338 | window.open(searchUrl.toString(), "_self"); 339 | }); 340 | } 341 | 342 | if (isFromSuggest || isFromSearch) { 343 | hideNativeSuggestions(); 344 | } else { 345 | showNativeSuggestions(); 346 | } 347 | }; 348 | 349 | (function() { 350 | 'use strict'; 351 | // adding our own type-ahead classes as twitter obfuscated native ones 352 | addPluginCSS(); 353 | 354 | const onSearchAdded = (searchEl) => { 355 | searchEl.addEventListener('input', onSearchChange); 356 | searchEl.addEventListener('change', onSearchChange); 357 | searchEl.addEventListener('keydown', onKeyDown); 358 | }; 359 | 360 | const onSearchRemoved = (searchEl) => { 361 | searchEl.removeEventListener('input', onSearchChange); 362 | searchEl.removeEventListener('change', onSearchChange); 363 | searchEl.removeEventListener('keydown', onKeyDown); 364 | } 365 | 366 | const observer = createObserver(onSearchAdded, onSearchRemoved); 367 | observer.observe(document.querySelector("#react-root"), { subtree: true, childList: true }); 368 | })(); --------------------------------------------------------------------------------