Расширение для Chrome, которое спасает от рейдов на Twitch
- суббота, 11 апреля 2026 г. в 00:01:03
Привет, Хабр
У многих из нас есть привычка засыпать под стримы или просто оставлять вкладку открытой. Любимый стример, спокойный голос, фоновая игра — идеальная атмосфера для сна. Но есть одна проблема: когда стример завершает эфир, он часто запускает рейд — массовое перенаправление зрителей на другой канал.
И вот вы просыпаетесь в 3 часа ночи от громкой музыки или незнакомой речи на каком-то случайном канале.
Можно, конечно, каждый раз вручную нажимать «Отменить» в окне рейда. А как это сделать, когда ты не успел нажать эту кнопку или вовсе спишь. Что если сделать это автоматически? Так появилось идея расширения «Twitch Raid Blocker».

В этой статье я расскажу:
Как устроены рейды на Twitch с технической точки зрения
Как расширение обнаруживает и блокирует их
Какие подводные камни встретились при разработке
Как работает архитектура расширения на Manifest V3
Рейд (raid) — это механизм, позволяющий стримеру перенаправить своих зрителей на другой канал в момент завершения трансляции. Для стримеров это способ поддержать коллег и обменяться аудиторией. Но для зрителей, которые уснули или отошли от компьютера, это может стать неприятным сюрпризом.
С технической точки зрения, при начале рейда Twitch показывает модальное окно с двумя вариантами:
Присоединиться к рейду (перейти на другой канал)
Отменить (остаться на текущей странице)
Наша задача — найти это окно и автоматически нажать кнопку «Отменить».

Расширение построено по стандартной схеме для Manifest V3 и состоит из четырёх основных компонентов:
├── manifest.json # Конфигурация ├── background.js # Service Worker (инициализация) ├── content.js # Content Script (основная логика) ├── popup.html/js # Интерфейс управления └── icons/ # Иконки
Начнём с конфигурации. Manifest V3 — это третья версия спецификации расширений Chrome, которая принесла ряд изменений в целях безопасности и производительности.
{ "manifest_version": 3, "name": "Котикс Блочит — Twitch Raid Blocker", "version": "2.1.2", "description": "Автоматически отменяет рейды на Twitch", "permissions": ["storage"], "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["https://*.twitch.tv/*"], "js": ["content.js"], "run_at": "document_idle" } ], "action": { "default_popup": "popup.html" } }
Ключевые моменты:
manifest_version: 3 — используем актуальную версию
permissions: ["storage"] — минимальные разрешения, только для хранения статистики
content_scripts — скрипт внедряется на все страницы Twitch
run_at: "document_idle" — скрипт запускается после полной загрузки страницы


Самая интересная часть — это content.js. Именно здесь происходит магия обнаружения и блокировки рейдов.
Twitch поддерживает множество языков, но нам достаточно покрыть русский и английский. Создадим массивы с ключевыми фразами:
const RAID_TEXTS = [ "проводит рейд", "начинает рейд", "вы присоединяетесь к рейду", "присоединяетесь к рейду", "вас перенаправляют", "скоро начнется рейд", "raiding", "starting raid", "you are joining a raid", "joining raid" ]; const CANCEL_TEXTS = [ "отменить", "остаться", "покинуть", "не переходить", "cancel", "stay here", "stay on channel", "don't join", "dismiss" ];
Текст на странице может содержать лишние пробелы, разные регистры и т.д. Приведём всё к единому виду:
function normalize(text) { return (text || "").replace(/\s+/g, " ").trim().toLowerCase(); } function textIncludesAny(text, patterns) { const t = normalize(text); return patterns.some(p => t.includes(normalize(p))); }
Нам нужно найти элемент, который:
Содержит текст о рейде
Имеет кнопку «Отменить»
Видим на странице
Имеет достаточный размер
function findRaidContainers() { const all = [...document.querySelectorAll("div, section")]; const result = []; for (const el of all) { if (processedContainers.has(el)) continue; if (!isVisible(el)) continue; const txt = normalize(el.innerText || el.textContent || ""); if (textIncludesAny(txt, RAID_TEXTS) && txt.length >= 15 && isLargeEnough(el) && isLikelyRaidModal(el)) { result.push(el); processedContainers.add(el); setTimeout(() => processedContainers.delete(el), CACHE_TTL); } } return result; }
Здесь используется Set для кэширования уже обработанных контейнеров — это предотвращает повторную обработку одних и тех же элементов. TTL (time-to-live) установлен в 15 секунд.
Важно убедиться, что элемент действительно виден пользователю:
function isVisible(el) { if (!el) return false; try { const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false; const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } catch (e) { return false; } }
Ищем кнопку внутри контейнера рейда:
function findCancelInside(container) { if (!container) return null; const buttons = container.querySelectorAll("button, [role='button']"); for (const btn of buttons) { const txt = normalize(btn.innerText || btn.textContent || btn.getAttribute("aria-label") || ""); if (!txt) continue; if (textIncludesAny(txt, CANCEL_TEXTS)) { return btn; } } return null; }
Обратите внимание: мы проверяем не только innerText, но и aria-label — это важно для доступности и случаев, когда текст кнопки скрыт визуально.
Финальный шаг — симуляция клика:
function clickElement(el, reason = "") { if (!el || !isVisible(el)) return false; try { el.click(); log("✅ Clicked:", reason); return true; } catch (e) { return false; } }
DOM на современных сайтах динамически обновляется. Рейд может появиться в любой момент, поэтому нам нужно постоянно мониторить изменения.
Используем два подхода:
MutationObserver — реагирует на изменения DOM
setInterval — страховочный опрос каждые 2 секунды
function startObserver() { if (observer) observer.disconnect(); if (scanInterval) clearInterval(scanInterval); try { observer = new MutationObserver(() => tryBlockRaid()); observer.observe(document.body || document.documentElement, { childList: true, subtree: true, attributes: false }); scanInterval = setInterval(() => tryBlockRaid(), 2000); console.log("[RAID BLOCK] ✅ Observer started"); } catch (e) { console.error("[RAID BLOCK] ❌ Observer failed:", e); } }
Почему оба метода? MutationObserver может пропустить некоторые изменения, особенно если они происходят очень быстро. Периодический опрос служит страховкой.
Чтобы не кликать по кнопке много раз подряд, введём ограничение:
let lastBlockTs = 0; function canBlockNow() { const now = Date.now(); if (now - lastBlockTs < 3000) return false; lastBlockTs = now; return true; }
Минимальный интервал между блокировками — 3 секунды. Этого достаточно, чтобы обработать один рейд и не создавать нагрузку.
Пользователям интересно знать, сколько рейдов было заблокировано. Используем chrome.storage.local:
function incrementCount() { raidBlockCount += 1; log("✅ Raid blocked! Count =", raidBlockCount); saveCount(); } function saveCount() { chrome.storage.local.set({ raidBlockCount }, () => { notifyPopup(); }); }
Данные сохраняются даже после перезагрузки браузера и синхронизируются с popup.
Интерфейс сделан минималистичным: переключатель включения/выключения, счётчик заблокированных рейдов и кнопка сброса.
<h1>🐱 КОТИКС БЛОЧИТ</h1> <div class="row"> <label for="toggleSwitch">Включить блокировку</label> <label class="switch"> <input type="checkbox" id="toggleSwitch"> <span class="slider"></span> </label> </div> <div class="status" id="statusText">Статус: ON</div> <div class="row counter"> RAID BLOCK: <span id="counterValue">0</span> </div> <button id="clearBtn">🗑️ Сбросить счётчик</button>
body { width: 280px; padding: 12px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0e0e10; color: #efeff1; margin: 0; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 2px; bottom: 2px; background-color: white; border-radius: 50%; transition: 0.3s; } input:checked + .slider { background-color: #f8a0a7; }
chrome.storage.onChanged.addListener((changes, area) => { if (area !== "local") return; if (changes.raidBlockCount) { counterValue.textContent = String(changes.raidBlockCount.newValue || 0); } if (changes.raidBlockEnabled) { const enabled = changes.raidBlockEnabled.newValue !== false; toggle.checked = enabled; statusText.textContent = "Статус: " + (enabled ? "ON" : "OFF"); } });
Это обеспечивает мгновенное обновление интерфейса при изменении данных из любого компонента расширения.
Service Worker в Manifest V3 заменяет собой старый background page. Он запускается при установке расширения и инициализирует значения по умолчанию:
chrome.runtime.onInstalled.addListener(async () => { const data = await chrome.storage.local.get([ "raidBlockEnabled", "raidBlockCount" ]); const updates = {}; if (typeof data.raidBlockEnabled === "undefined") { updates.raidBlockEnabled = true; } if (typeof data.raidBlockCount === "undefined") { updates.raidBlockCount = 0; } if (Object.keys(updates).length > 0) { await chrome.storage.local.set(updates); } });
Twitch — это SPA (Single Page Application) на React. Элементы постоянно перерисовываются, классы могут меняться. Поэтому мы не полагаемся на конкретные селекторы или классы, а используем текстовый анализ и семантические признаки (наличие кнопки отмены, размер элемента).
Первоначально расширение могло реагировать на любые упоминания слова «рейд» в чате. Решение: требовать наличие кнопки отмены внутри контейнера и минимальный размер элемента (150×50 пикселей).
Первоначальная версия сканировала весь DOM каждую секунду. Это создавало нагрузку. Оптимизации:
Кэширование обработанных контейнеров (TTL 15 секунд)
Увеличение интервала опроса до 2 секунд
Ранний выход при отсутствии видимых элементов
В Manifest V3 background script работает как Service Worker и может быть остановлен в любой момент. Поэтому вся критичная логика вынесена в content script, который работает непосредственно на странице.
Расширение запрашивает минимальные разрешения:
storage — только для хранения локальной статистики
Не собирает никакие персональные данные
Не отправляет данные на внешние серверы
Весь код выполняется локально в браузере
Исходный код открыт и доступен для аудита.
Если вы тоже страдаете от ночных рейдов — надеюсь, это расширение вам поможет. А если вы разработчик — возможно, некоторые приёмы из статьи пригодятся в ваших проектах.
Ссылка на репозиторий: GitHub