- PVSM.RU - https://www.pvsm.ru -
Привет, Хабр
У многих из нас есть привычка засыпать под стримы или просто оставлять вкладку открытой. Любимый стример, спокойный голос, фоновая игра — идеальная атмосфера для сна. Но есть одна проблема: когда стример завершает эфир, он часто запускает рейд — массовое перенаправление зрителей на другой канал.
И вот вы просыпаетесь в 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 [1]
Автор: raptortal
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/google-chrome-2/449474
Ссылки в тексте:
[1] GitHub: https://github.com/raptortal/twitch-raid-blocker
[2] Источник: https://habr.com/ru/articles/1021824/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1021824
Нажмите здесь для печати.