├── 2021-09-26 05-26-07.2021-09-26 05_27_32.gif ├── .github └── FUNDING.yml ├── README.md └── twitchmassban.user.js /2021-09-26 05-26-07.2021-09-26 05_27_32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victornpb/twitch-mass-ban/HEAD/2021-09-26 05-26-07.2021-09-26 05_27_32.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | # These are supported funding model platforms 3 | 4 | custom: ['https://www.buymeacoffee.com/vitim'] 5 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 6 | patreon: # Replace with a single Patreon username 7 | open_collective: # Replace with a single Open Collective username 8 | ko_fi: victornpb 9 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 10 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 11 | liberapay: # Replace with a single Liberapay username 12 | issuehunt: # Replace with a single IssueHunt username 13 | otechie: # Replace with a single Otechie username 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧑‍⚖️ Twitch RaidHammer - An utility for Easily banning multiple hate raid accounts 2 | Everytime someone follows the window should appear, then you can choose to: 3 | - ? view account age 4 | - IGNORE legit users 5 | - BAN accounts 6 | - BAN ALL to give them all the **🚷 BAN HAMMER**! 7 | 8 | ![](https://user-images.githubusercontent.com/3372598/134840805-af6eef0e-898e-4b74-867e-add1f53edc93.gif) 9 | 10 | # Instructions 11 | 12 | 13 | 1. First you need a Browser Extension for managing UserScripts[[1]][userscrips_faq] (skip if you already have one): 14 | * Chrome: [Violentmonkey][chrome_violentmonkey] or [Tampermonkey][chrome_tampermonkey] 15 | * Brave: [Violentmonkey][chrome_violentmonkey] or [Tampermonkey][chrome_tampermonkey] 16 | * Firefox: [Greasemonkey][firefox_greasemonkey], [Tampermonkey][firefox_tampermonkey], or [Violentmonkey][firefox_violentmonkey] 17 | * Opera: [Tampermonkey][opera_tampermonkey] or [Violentmonkey][opera_violentmonkey] 18 | * Edge: [Tampermonkey][edge_tampermonkey] 19 | * Safari: ~[Tampermonkey][safari_tampermonkey]~ 20 | 21 | 1. Install Twitch RaidHammer: 22 | [![][greasyfork_icon]][greasyfork_url] or [![][openuserjs_icon]][openuserjs_url] 23 | 24 | 1. (Optional) The feature that monitors new followers as they come, 25 | requires that you have [StreamElements](https://streamelements.com/features/chatbot) or [StreamLabs](https://streamlabs.com/content-hub/tag/chatbot) Bot announcing follow alerts in the chat: 26 | (This is not required if you just want to use the mass banning feature). 27 | You should have the bots announce one of these messages: 28 | - > **Streamlabs**: Thank you for following `username`! 29 | - > **StreamElements**: Welcome! `username` Thank you for following! 30 | 31 | 32 | 33 | 1. You're all set! 34 | 35 | Look for the hammer icon next to Twitch chat. [see notes](#notes) 36 | 37 | Note: It will ONLY appear on streams you have moderation privilegies. 38 | 39 | ---- 40 | 41 | # Import List of Bots 42 | 43 | You can also import a list of users and ban all of them with only 3 clicks! 44 | No more coping and pasting hundreds of /ban commands. 45 | 46 | ![Mass banning list of bots](https://user-images.githubusercontent.com/3372598/165885461-a997f0f1-e880-4390-8b9e-77a95e707833.gif) 47 | 48 | 49 | ---- 50 | 51 | If you have issues or just need help [open a discussion here](https://github.com/victornpb/twitch-mass-ban/discussions) 52 | 53 | 54 | 55 | [userscrips_faq]: https://en.wikipedia.org/wiki/Userscript 56 | [greasyfork_icon]: https://user-images.githubusercontent.com/3372598/166113712-1bc3d654-1342-4f1e-9845-21c3b21524b1.png 57 | [openuserjs_icon]: https://user-images.githubusercontent.com/3372598/166113714-5a2ede39-8d66-43a8-b5da-8f1897cb3121.png 58 | 59 | [chrome_violentmonkey]: https://chrome.google.com/webstore/detail/violent-monkey/jinjaccalgkegednnccohejagnlnfdag 60 | [chrome_tampermonkey]: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo 61 | [firefox_greasemonkey]: https://addons.mozilla.org/firefox/addon/greasemonkey/ 62 | [firefox_tampermonkey]: https://addons.mozilla.org/firefox/addon/tampermonkey/ 63 | [firefox_violentmonkey]: https://addons.mozilla.org/firefox/addon/violentmonkey/ 64 | [safari_tampermonkey]: https://github.com/victornpb/undiscord/issues/91#issuecomment-654514364 65 | [edge_tampermonkey]: https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd 66 | [opera_tampermonkey]: https://addons.opera.com/extensions/details/tampermonkey-beta/ 67 | [opera_violentmonkey]: https://addons.opera.com/extensions/details/violent-monkey/ 68 | 69 | 70 | [greasyfork_url]: "Get Twitch RaidHammer from GreasyFork" 71 | [openuserjs_url]: "Get Twitch RaidHammer from OpenUserJS" 72 | -------------------------------------------------------------------------------- /twitchmassban.user.js: -------------------------------------------------------------------------------- 1 | 2 | // ==UserScript== 3 | // @name Twitch RaidHammer - Easily ban multiple accounts during hate raids 4 | // @description A tool for moderating Twitch easier during hate raids 5 | // @namespace https://github.com/victornpb/twitch-mass-ban 6 | // @version 1.1.4 7 | // @match *://*.twitch.tv/* 8 | // @run-at document-idle 9 | // @author victornpb 10 | // @homepageURL https://github.com/victornpb/twitch-mass-ban 11 | // @supportURL https://github.com/victornpb/twitch-mass-ban/discussions 12 | // @contributionURL https://www.buymeacoffee.com/vitim 13 | // @grant none 14 | // @license MIT 15 | // ==/UserScript== 16 | 17 | /* jshint esversion: 8 */ 18 | 19 | (function () { 20 | var html = /*html*/` 21 |
22 | 120 |
121 | 122 | 126 | 127 | 128 |
129 | 137 |
138 |
139 | Usernames 140 |
141 |
142 |
143 | 144 | 145 | 146 |
147 |
148 | 150 |
151 | `; 152 | const LOGPREFIX = '[RAIDHAMMER]'; 153 | 154 | // modal 155 | const d = document.createElement("div"); 156 | d.style.display = 'none'; 157 | d.innerHTML = html; 158 | const textarea = d.querySelector("textarea"); 159 | 160 | // activation button 161 | const activateBtn = document.createElement('button'); 162 | activateBtn.innerHTML = ` 163 | 164 | 165 | 166 | `; 167 | activateBtn.style.cssText = ` 168 | display: inline-flex; 169 | -webkit-box-align: center; 170 | align-items: center; 171 | -webkit-box-pack: center; 172 | justify-content: center; 173 | user-select: none; 174 | height: var(--button-size-default); 175 | width: var(--button-size-default); 176 | border-radius: var(--border-radius-medium); 177 | background-color: var(--color-background-button-text-default); 178 | color: var(--color-fill-button-icon); 179 | `; 180 | activateBtn.setAttribute('title', 'RaidHammer'); 181 | activateBtn.onclick = toggle; 182 | 183 | let enabled; 184 | let watchdogTimer; 185 | 186 | function appendActivatorBtn() { 187 | const modBtn = document.querySelector('[data-test-selector="mod-view-link"]'); 188 | if (modBtn) { 189 | const twitchBar = modBtn.parentElement.parentElement.parentElement; 190 | if (twitchBar && !twitchBar.contains(activateBtn)) { 191 | console.log(LOGPREFIX, 'Mod tools available. Adding button...'); 192 | twitchBar.insertBefore(activateBtn, twitchBar.firstChild); 193 | document.body.appendChild(d); 194 | if (!enabled) { 195 | console.log(LOGPREFIX, 'Started chatWatchdog...'); 196 | watchdogTimer = setInterval(chatWatchdog, 500); 197 | enabled = true; 198 | } 199 | } 200 | 201 | } else if (document.location.toString().includes('/moderator/')){ 202 | const chatBtn = document.querySelector('[data-a-target="chat-send-button"]'); 203 | const twitchBar = chatBtn.parentElement.parentElement.parentElement; 204 | if (twitchBar && !twitchBar.contains(activateBtn)) { 205 | console.log(LOGPREFIX, 'Mod tools available. Adding button...'); 206 | twitchBar.insertBefore(activateBtn, twitchBar.firstChild); 207 | document.body.appendChild(d); 208 | if (!enabled) { 209 | console.log(LOGPREFIX, 'Started chatWatchdog...'); 210 | watchdogTimer = setInterval(chatWatchdog, 500); 211 | enabled = true; 212 | } 213 | } 214 | } 215 | else { 216 | if (enabled) { 217 | console.log(LOGPREFIX, 'Mod tools not found. Stopped chatWatchdog!'); 218 | clearInterval(watchdogTimer); 219 | watchdogTimer = enabled = false; 220 | hide(); 221 | } 222 | } 223 | } 224 | setInterval(appendActivatorBtn, 5000); 225 | 226 | 227 | //events 228 | d.querySelector(".ignoreAll").onclick = ignoreAll; 229 | d.querySelector(".banAll").onclick = banAll; 230 | d.querySelector(".closeBtn").onclick = hide; 231 | 232 | d.querySelector(".import button.importBtn").onclick = importList; 233 | d.querySelector(".import button.cancelBtn").onclick = toggleImport; 234 | 235 | // delegated events 236 | d.addEventListener('click', e => { 237 | const target = e.target; 238 | if (target.matches('.ignore')) ignoreItem(target.dataset.user); 239 | if (target.matches('.ban')) banItem(target.dataset.user); 240 | if (target.matches('.accountage')) accountage(target.dataset.user); 241 | if (target.matches('.toggleImport')) toggleImport(); 242 | 243 | }); 244 | 245 | const delay = t => new Promise(r => setTimeout(r, t)); 246 | 247 | function show() { 248 | console.log(LOGPREFIX, 'Show'); 249 | d.style.display = ''; 250 | renderList(); 251 | } 252 | 253 | function hide() { 254 | console.log(LOGPREFIX, 'Hide'); 255 | d.style.display = 'none'; 256 | } 257 | 258 | function toggle() { 259 | if (d.style.display !== 'none') hide(); 260 | else show(); 261 | } 262 | 263 | function toggleImport() { 264 | const importDiv = d.querySelector(".import"); 265 | const body = d.querySelector(".body"); 266 | if (importDiv.style.display !== 'none') { 267 | importDiv.style.display = 'none'; 268 | body.style.display = ''; 269 | } 270 | else { 271 | importDiv.style.display = ''; 272 | body.style.display = 'none'; 273 | d.querySelector(".import textarea").focus(); 274 | } 275 | } 276 | 277 | function importList() { 278 | const textarea = d.querySelector(".import textarea"); 279 | const lines = textarea.value.split(/\n/).map(line => line.trim()).filter(Boolean); 280 | for (const line of lines) { 281 | if (/^[\w_]+$/.test(line)) queueList.add(line); 282 | } 283 | textarea.value = ''; 284 | toggleImport(); 285 | renderList(); 286 | } 287 | 288 | let queueList = new Set(); 289 | let ignoredList = new Set(); 290 | let bannedList = new Set(); 291 | 292 | function chatWatchdog() { 293 | const recentNames = extractRecent(); 294 | if (recentNames.length) { 295 | const newNames = recentNames 296 | .filter(name => !queueList.has(name)) 297 | .filter(name => !ignoredList.has(name)) 298 | .filter(name => !bannedList.has(name)); 299 | 300 | if (newNames.length) { 301 | newNames.forEach(name => queueList.add(name)); 302 | onFollower(); 303 | } 304 | } 305 | } 306 | 307 | function parseChat() { 308 | return Array.from(document.querySelectorAll('[data-test-selector="chat-line-message"]')).map(chat => { 309 | return { 310 | username: chat.querySelector('[data-test-selector="message-username"]').innerText, 311 | message: chat.querySelector('[data-test-selector="chat-line-message-body"]').innerText, 312 | // timestamp: chat.querySelector('[data-test-selector="chat-timestamp"]').innerText, 313 | }; 314 | }); 315 | } 316 | 317 | function extractRecent() { 318 | let newFollowers = new Set(); 319 | const messages = parseChat().filter(m => m.username === 'StreamElements' || m.username === 'Streamlabs'); 320 | for (const { message } of messages) { 321 | const match = ( 322 | message.match(/Thank you for following ([\w_]+)/) || 323 | message.match(/Welcome! ([\w_]+) Thank you for following!/) 324 | ); 325 | if (match) newFollowers.add(match[1]); 326 | } 327 | 328 | return [...newFollowers]; 329 | } 330 | 331 | function onFollower() { 332 | console.log(LOGPREFIX, 'onFollower', queueList); 333 | renderList(); 334 | show(); 335 | } 336 | 337 | function ignoreAll() { 338 | console.log(LOGPREFIX, 'Ignoring all...', queueList); 339 | for (const user of queueList) { 340 | ignoreItem(user); 341 | } 342 | } 343 | 344 | async function banAll() { 345 | console.log(LOGPREFIX, 'Banning all...', queueList); 346 | for (const user of queueList) { 347 | banItem(user); 348 | await delay(250); 349 | } 350 | } 351 | 352 | function accountage(user) { 353 | console.log(LOGPREFIX, 'Accountage', user); 354 | sendMessage('!accountage ' + user); 355 | } 356 | 357 | function ignoreItem(user) { 358 | console.log(LOGPREFIX, 'Ignored user', user); 359 | queueList.delete(user); 360 | ignoredList.add(user); 361 | renderList(); 362 | if (queueList.size === 0) hide(); // auto hide on the last 363 | } 364 | 365 | function banItem(user) { 366 | console.log(LOGPREFIX, 'Ban user', user); 367 | queueList.delete(user); 368 | bannedList.add(user); 369 | sendMessage('/ban ' + user); 370 | renderList(); 371 | } 372 | 373 | function sendMessage(msg) { 374 | try{ 375 | sendMessageOld(msg); 376 | } 377 | catch(_){ 378 | sendMessageSlate(msg); 379 | } 380 | } 381 | 382 | function sendMessageOld(msg) { 383 | const textarea = document.querySelector("[data-a-target='chat-input']"); 384 | const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 385 | nativeTextAreaValueSetter.call(textarea, msg); 386 | const event = new Event('input', { bubbles: true }); 387 | textarea.dispatchEvent(event); 388 | document.querySelector("[data-a-target='chat-send-button']").click(); 389 | } 390 | 391 | function sendMessageSlate(msg) { 392 | function _injectInput(el, data) { 393 | [ 394 | 'keydown', 395 | 'beforeinput', 396 | //'input', 397 | ].forEach((event, i) => { 398 | const eventObj = { 399 | altKey: false, 400 | charCode: 0, 401 | ctrlKey: false, 402 | metaKey: false, 403 | shiftKey: false, 404 | which: '', 405 | keyCode: '', 406 | data: data, 407 | inputType: 'insertText', 408 | key: data, 409 | }; 410 | el.dispatchEvent(new InputEvent(event, eventObj)); 411 | }); 412 | } 413 | 414 | function _triggerKeyboardEvent(el, keyCode) { 415 | const eventObj = document.createEventObject ? document.createEventObject() : document.createEvent("Events"); 416 | if (eventObj.initEvent) { 417 | eventObj.initEvent("keydown", true, true); 418 | } 419 | eventObj.keyCode = keyCode; 420 | eventObj.which = keyCode; 421 | el.dispatchEvent ? el.dispatchEvent(eventObj) : el.fireEvent("onkeydown", eventObj); 422 | } 423 | 424 | const editor = document.querySelector('[data-slate-editor="true"]'); 425 | editor.focus(); 426 | _injectInput(editor, msg); 427 | _triggerKeyboardEvent(editor, 13); 428 | } 429 | 430 | 431 | function renderList() { 432 | d.querySelector(".ignoreAll").style.display = queueList.size ? '' : 'none'; 433 | d.querySelector(".banAll").style.display = queueList.size ? '' : 'none'; 434 | const renderItem = item => ` 435 |
  • 436 | 437 | 438 | 439 | ${item} 440 |
  • 441 | `; 442 | 443 | let inner = queueList.size ? [...queueList].map(user => renderItem(user)).join('') : ` 444 |
    445 |

    Recent followers is empty :)

    446 |

    Automatically listening for new followers...

    447 |

    448 | 449 |
    `; 450 | 451 | d.querySelector('.list').innerHTML = ` 452 | 455 | `; 456 | } 457 | 458 | })(); 459 | --------------------------------------------------------------------------------