Я считаю что ИИ без твоей экспертизы это ноль.
— /2025/10/07/ford-ceo-reveals-why-he-sent-his-son-to-work-as-a-welder/#comment-52591
Попробуем это опровергнуть?
В сети повсеместно встречаются мнения, что "ИИ тупой", "ничего не умеет", программистов он не заменит и, что самое главное - якобы никто не пострадает от этой "скатерти-самобранки".
Я не JavaScript программист, мои познания в этом языке остались на уровне 2015 года. О технологии WebRTC ничего не знаю. Нашёл анонимный видеочат без регистрации https://chatroulett.ru и поставил цель - добавить блокировку пользователей одной кнопкой. В качестве инструмента - https://chat.deepseek.com.
За пару часов мучений, связанных с перехватом WebRTC и корректным обнаружением IP-адресов кандидатов, мы пришли к работающему нетривиальному коду. И это не какой-то там CRUD, не багфиксы на несколько строк, а полноценная работающая программа со сложной логикой.
Профессия "программист" в данном виде существует последние годы.
Общее описание
Скрипт представляет собой довольно сложный инструмент для "управления" видеочатом chatroulett.ru. Он вмешивается в низкоуровневый процесс установления WebRTC-соединений (используя RTCPeerConnection, setRemoteDescription, addIceCandidate), анализирует сетевые параметры (IP-адреса из SDP и ICE-кандидатов) и блокирует соединения на основе пользовательского черного списка, хранящегося в localStorage. Он также предоставляет удобный интерфейс (кнопка "BAN") для добавления IP-адресов в черный список вручную и автоматически переходит к следующему собеседнику при обнаружении или добавлении заблокированного IP:

Программный код (Java Script):
// ==UserScript==
// @name Chatroulett WebRTC Blocker
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Мгновенная блокировка нежелательных WebRTC соединений для chatroulett.ru
// @author Your Name
// @match *://chatroulett.ru/*
// @match *://*.chatroulett.ru/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// Инициализация черного списка
const STORAGE_KEY = 'webrtc_blacklist';
const RECENT_IPS_KEY = 'webrtc_recent_ips';
// Функция для получения актуального черного списка
const getBlacklist = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
// Разделенные списки исключений
const EXCLUDED_DOMAINS = [
'chatroulett.ru',
'api.chatroulett.ru',
'webrtc.chatroulett.ru'
];
const EXCLUDED_IP_RANGES = [
'0.0.0.0/8', // Текущая сеть
'10.0.0.0/8', // Частная сеть
'100.64.0.0/10', // CGNAT
'127.0.0.0/8', // Loopback
'169.254.0.0/16', // Link-local
'172.16.0.0/12', // Частная сеть
'192.0.0.0/24', // Зарезервировано IANA
'192.0.2.0/24', // TEST-NET-1
'192.168.0.0/16', // Частная сеть
'198.18.0.0/15', // Тестовые сети
'198.51.100.0/24', // TEST-NET-2
'203.0.113.0/24', // TEST-NET-3
'224.0.0.0/4', // Multicast
'240.0.0.0/4', // Зарезервировано
'255.255.255.255/32', // Broadcast
'7.0.0.0/8', // Министерство обороны США
'26.0.0.0/8', // Министерство обороны США
'28.0.0.0/8', // Министерство обороны США
'14.0.0.0/8', // Китай (публичная сеть, но часто используется для NAT)
// подсеть хостинга видеочата
'93.189.228.0/22'
];
// Функция для преобразования IP в число
const ipToNumber = (ip) => {
const parts = ip.split('.');
if (parts.length !== 4) return 0;
let result = 0;
for (let i = 0; i < 4; i++) {
const part = parseInt(parts[i], 10);
if (isNaN(part) || part < 0 || part > 255) return 0;
result += part * Math.pow(256, 3 - i);
}
return result;
};
// Функция проверки диапазона в CIDR нотации
const isIPInRangeCIDR = (ip, cidr) => {
const [network, prefix] = cidr.split('/');
const prefixLength = parseInt(prefix, 10);
const ipNum = ipToNumber(ip);
const networkNum = ipToNumber(network);
const mask = ~((1 << (32 - prefixLength)) - 1) >>> 0;
return (ipNum & mask) === (networkNum & mask);
};
// Улучшенная функция проверки валидности IP
const isValidIP = (ip) => {
if (typeof ip !== 'string' || ip.trim() === '') {
return false;
}
if (/[^0-9a-fA-F.:]/.test(ip)) {
return false;
}
// IPv4
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (ipv4Regex.test(ip)) {
return true;
}
// IPv6 (упрощенная проверка)
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
const ipv6ShortRegex = /^((?:[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})*)?)::((?:[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})*)?)$/;
if (ipv6Regex.test(ip) || ipv6ShortRegex.test(ip)) {
return true;
}
return false;
};
// Улучшенная функция проверки исключений
const isExcluded = (value) => {
// Проверка специальных адресов
if (value === '0.0.0.0' ||
value === '::' ||
value === '0000:0000:0000:0000:0000:0000:0000:0000' ||
value === '255.255.255.255' ||
value.startsWith('192.0.2.') ||
value.startsWith('198.51.100.') ||
value.startsWith('203.0.113.') ||
value.startsWith('169.254.')
) {
return true;
}
// Проверка доменов
if (!isValidIP(value)) {
return EXCLUDED_DOMAINS.some(domain => value.includes(domain));
}
// Проверка CIDR диапазонов
for (const cidr of EXCLUDED_IP_RANGES) {
if (isIPInRangeCIDR(value, cidr)) {
return true;
}
}
return false;
};
// Функция для сохранения черного списка
const saveBlacklist = (blacklist) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(blacklist));
};
// Функция для добавления IP в список последних
const addToRecentIPs = (ip) => {
if (!isValidIP(ip) || isExcluded(ip)) {
return;
}
let recentIPs = JSON.parse(localStorage.getItem(RECENT_IPS_KEY) || '[]');
recentIPs = recentIPs.filter(existingIp => existingIp !== ip);
recentIPs.push(ip);
if (recentIPs.length > 10) {
recentIPs = recentIPs.slice(-10);
}
localStorage.setItem(RECENT_IPS_KEY, JSON.stringify(recentIPs));
};
// Функция для принудительной очистки черного списка от исключенных IP
const forceCleanBlacklist = () => {
let blacklist = getBlacklist();
const originalLength = blacklist.length;
const cleanedBlacklist = blacklist.filter(ip => {
if (isExcluded(ip)) {
return false;
}
return isValidIP(ip);
});
if (cleanedBlacklist.length !== originalLength) {
saveBlacklist(cleanedBlacklist);
}
return cleanedBlacklist;
};
// Функция для отображения сообщения
const showMessage = (text, backgroundColor = 'red', textColor = 'white') => {
document.querySelectorAll('.blocker-message').forEach(msg => msg.remove());
const message = document.createElement('div');
message.className = 'blocker-message';
Object.assign(message.style, {
position: 'fixed',
bottom: '20px',
left: '20px',
padding: '15px 25px',
backgroundColor: backgroundColor,
color: textColor,
borderRadius: '5px',
fontSize: '18px',
fontWeight: 'bold',
zIndex: 10000,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
transition: 'opacity 0.5s ease-out'
});
message.textContent = text;
document.body.appendChild(message);
setTimeout(() => {
message.style.opacity = '0';
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 500);
}, 6000);
};
// Функция для извлечения IP из ICE кандидата
const extractIPFromCandidate = (candidate) => {
const regex = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
const match = candidate.match(regex);
return match ? match[0] : null;
};
// Функция для извлечения IPv4 из IPv6 адресов
const extractIPv4FromIPv6 = (ipv6) => {
const ipv4MappedRegex = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/;
const mappedMatch = ipv6.match(ipv4MappedRegex);
if (mappedMatch) return mappedMatch[1];
const ipv4CompatRegex = /^::(\d+\.\d+\.\d+\.\d+)$/;
const compatMatch = ipv6.match(ipv4CompatRegex);
if (compatMatch) return compatMatch[1];
const nat64Regex = /^64:ff9b::(\d+\.\d+\.\d+\.\d+)$/;
const nat64Match = ipv6.match(nat64Regex);
if (nat64Match) return nat64Match[1];
return null;
};
// Функция проверки блокировки IP
const shouldBlockIP = (ip) => {
if (!ip || typeof ip !== 'string') {
return false;
}
// Сначала проверяем исключения
if (isExcluded(ip)) {
return false;
}
const blacklist = getBlacklist();
if (!ip.includes(':')) {
return blacklist.includes(ip);
}
const extractedIPv4 = extractIPv4FromIPv6(ip);
if (extractedIPv4) {
return blacklist.includes(extractedIPv4);
}
return false;
};
// Функция для поиска и клика по кнопке ДАЛЬШЕ
const clickNextButton = () => {
// Основной селектор для кнопки ДАЛЬШЕ на chatroulett.ru
const nextButton = document.querySelector('button.btn.btn-primary.btn-lg.d-block.w-100.fs-2.px-0.py-3.py-lg-5');
if (nextButton) {
if (nextButton.disabled) {
nextButton.disabled = false;
}
nextButton.click();
return true;
}
// Альтернативные селекторы для кнопки ДАЛЬШЕ
const alternativeSelectors = [
'button[class*="next"]',
'button[class*="primary"]',
'.next-btn',
'[class*="next"]'
];
for (const selector of alternativeSelectors) {
const button = document.querySelector(selector);
if (button) {
if (button.disabled) {
button.disabled = false;
}
button.click();
return true;
}
}
return false;
};
// Наблюдатель за кнопкой ДАЛЬШЕ - снимает disabled если он появляется
const observeNextButton = () => {
const observer = new MutationObserver(() => {
const nextButton = document.querySelector('button.btn.btn-primary.btn-lg.d-block.w-100.fs-2.px-0.py-3.py-lg-5');
if (nextButton && nextButton.disabled) {
nextButton.disabled = false;
}
const alternativeButtons = document.querySelectorAll('button[class*="next"], button[class*="primary"]');
alternativeButtons.forEach(button => {
if (button.disabled) {
button.disabled = false;
}
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['disabled', 'class']
});
return observer;
};
// Функция анализа SDP
const analyzeSDP = (sdp) => {
const foundIPs = new Set();
if (!sdp) return foundIPs;
const ipv4Regex = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g;
const matches = sdp.match(ipv4Regex);
if (matches) {
matches.forEach(ip => {
if (isValidIP(ip) && !isExcluded(ip)) {
foundIPs.add(ip);
}
});
}
return foundIPs;
};
// Создание кнопки блокировки
const createButton = () => {
const banButton = document.createElement('button');
Object.assign(banButton.style, {
position: 'fixed',
top: '50px',
left: '45px',
width: '120px',
height: '120px',
borderRadius: '50%',
backgroundColor: 'red',
color: 'white',
border: 'none',
cursor: 'pointer',
zIndex: 9999,
fontSize: '12px',
fontWeight: 'bold',
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
});
banButton.textContent = 'BAN';
banButton.title = 'Заблокировать текущего собеседника';
document.body.appendChild(banButton);
return banButton;
};
// === Перехват RTCPeerConnection выполняется сразу на document-start ===
let activeConnections = [];
const OriginalRTCPeerConnection = window.RTCPeerConnection ||
window.webkitRTCPeerConnection ||
window.mozRTCPeerConnection;
if (OriginalRTCPeerConnection) {
const createBlockedError = (ip) => {
const error = new Error(`Connection blocked: IP ${ip} in blacklist`);
error.name = 'BlockedConnectionError';
return error;
};
window.RTCPeerConnection = function(...args) {
const pc = new OriginalRTCPeerConnection(...args);
// Перехват setRemoteDescription
const originalSetRemoteDescription = pc.setRemoteDescription.bind(pc);
pc.setRemoteDescription = function(description) {
if (description && description.sdp) {
const foundIPs = analyzeSDP(description.sdp);
for (const ip of foundIPs) {
if (shouldBlockIP(ip)) {
showMessage('Блокировка: ' + ip, 'orange', 'white');
pc.close();
setTimeout(clickNextButton, 100);
return Promise.reject(createBlockedError(ip));
}
}
}
return originalSetRemoteDescription(description);
};
// Перехват addIceCandidate
const originalAddIceCandidate = pc.addIceCandidate.bind(pc);
pc.addIceCandidate = function(candidate) {
if (candidate && candidate.candidate) {
const ip = extractIPFromCandidate(candidate.candidate);
if (ip && shouldBlockIP(ip)) {
showMessage('Блокировка: ' + ip, 'orange', 'white');
pc.close();
setTimeout(clickNextButton, 100);
return Promise.reject(createBlockedError(ip));
}
}
return originalAddIceCandidate(candidate);
};
// Отслеживание активных соединений
pc.addEventListener('connectionstatechange', () => {
if (['closed', 'disconnected', 'failed'].includes(pc.connectionState)) {
const index = activeConnections.indexOf(pc);
if (index !== -1) activeConnections.splice(index, 1);
}
});
activeConnections.push(pc);
return pc;
};
// Копируем прототип
Object.setPrototypeOf(window.RTCPeerConnection, OriginalRTCPeerConnection);
window.RTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
}
// === Инициализация UI после загрузки DOM ===
const initUI = () => {
// Принудительно очищаем черный список от исключенных IP
forceCleanBlacklist();
const banButton = createButton();
const nextButtonObserver = observeNextButton();
// Обработчик клика по кнопке блокировки
banButton.addEventListener('click', () => {
let currentBlacklist = getBlacklist();
let foundIPs = new Set();
let alreadyBlockedIPs = new Set();
let newBlockedIPs = [];
// Анализ активных соединений
for (const pc of activeConnections) {
try {
if (pc.remoteDescription && pc.remoteDescription.sdp) {
const sdpIPs = analyzeSDP(pc.remoteDescription.sdp);
sdpIPs.forEach(ip => {
if (shouldBlockIP(ip)) {
alreadyBlockedIPs.add(ip);
} else if (!isExcluded(ip)) {
foundIPs.add(ip);
}
});
}
} catch (e) {
// Игнорируем ошибки анализа
}
}
// Добавление новых IP в черный список
let addedCount = 0;
if (foundIPs.size > 0) {
foundIPs.forEach(ip => {
if (!currentBlacklist.includes(ip)) {
currentBlacklist.push(ip);
newBlockedIPs.push(ip);
addedCount++;
addToRecentIPs(ip);
}
});
if (addedCount > 0) {
saveBlacklist(currentBlacklist);
if (newBlockedIPs.length > 0) {
showMessage('Заблокировано: ' + newBlockedIPs.join(', '), 'red', 'white');
}
// Закрываем все активные соединения
activeConnections.forEach(pc => pc.close());
activeConnections = [];
}
}
// Обработка уже заблокированных IP
if (alreadyBlockedIPs.size > 0 && addedCount === 0) {
const blockedList = Array.from(alreadyBlockedIPs);
if (blockedList.length > 0) {
showMessage('Уже заблокировано: ' + blockedList.join(', '), 'orange', 'white');
}
// Закрываем все активные соединения
activeConnections.forEach(pc => pc.close());
activeConnections = [];
}
// Если не нашли IP вообще
if (foundIPs.size === 0 && alreadyBlockedIPs.size === 0) {
showMessage('IP не найден — соединение ещё не установлено', 'gray', 'white');
}
// Переход к следующему собеседнику
clickNextButton();
});
};
// Запуск UI-инициализации после загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUI);
} else {
initUI();
}
})();
Chatroulett WebRTC Blocker — техническая документация
Техническая архитектура
1. Слой перехвата WebRTC соединений
- Monkey-patching RTCPeerConnection — замена глобального конструктора
RTCPeerConnectionна обёртку, которая перехватывает создание всех новых соединений - Перехват выполняется на этапе
document-start, до загрузки DOM, чтобы гарантировать перехват всех соединений, создаваемых страницей - Двухфазная блокировка:
- Pre-connection: анализ SDP в
setRemoteDescription()до установки соединения - Mid-connection: проверка IP в
addIceCandidate()во время ICE negotiation
- Pre-connection: анализ SDP в
- Каждое созданное соединение добавляется в массив
activeConnectionsи удаляется из него при переходе в состояниеclosed,disconnectedилиfailed
2. Система обнаружения IP-адресов
Анализ SDP (Session Description Protocol)
- Функция
analyzeSDP()выполняет полнотекстовый поиск IPv4-адресов во всём теле SDP с помощью регулярного выражения - Извлекаются только валидные и не исключённые адреса (проверка через
isValidIP()иisExcluded())
Обработка ICE кандидатов
- Функция
extractIPFromCandidate()извлекает первый IPv4-адрес из строки ICE-кандидата - Перехват происходит в обёртке
addIceCandidate()— при обнаружении заблокированного IP соединение немедленно закрывается
3. Валидация и классификация IP-адресов
Валидация IPv4:
- Проверка формата
xxx.xxx.xxx.xxx(каждый октет 0–255) через регулярное выражение - Дополнительная проверка на отсутствие посторонних символов (допускаются только
0-9,a-f,A-F,.,:)
Обработка IPv6:
- Базовая валидация полного и сокращённого форматов IPv6
- IPv4-mapped IPv6:
::ffff:192.168.1.1→ извлечение IPv4-компонента - IPv4-compatible IPv6:
::192.168.1.1→ конвертация в чистый IPv4 - NAT64 трансляция:
64:ff9b::192.168.1.1→ декомпозиция IPv4-части - Чистые IPv6 без IPv4-компонента → игнорируются (блокировка не применяется)
4. Система управления чёрными списками
Хранение:
- Чёрный список хранится в
localStorageпод ключомwebrtc_blacklist(массив IP-адресов в формате JSON) - Список последних обнаруженных IP хранится под ключом
webrtc_recent_ips(до 10 записей)
Очистка при запуске:
- Функция
forceCleanBlacklist()при инициализации удаляет из чёрного списка невалидные и исключённые IP-адреса
CIDR-нотация для диапазонов исключений:
const EXCLUDED_IP_RANGES = [
'0.0.0.0/8',
'10.0.0.0/8',
'127.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'93.189.228.0/22', // подсеть хостинга видеочата
// ...
];
Алгоритм проверки диапазонов:
- Преобразование IP и адреса сети в 32-битные числа
- Формула преобразования:
octet1 × 256³ + octet2 × 256² + octet3 × 256 + octet4 - Битовая маска:
~((1 << (32 - prefix)) - 1) >>> 0 - Проверка принадлежности:
(IP & mask) === (network & mask)
5. Многоуровневая система исключений
Иерархия проверок в функции isExcluded():
- Специальные адреса:
0.0.0.0,::,255.255.255.255 - Префиксные проверки:
192.0.2.*,198.51.100.*,203.0.113.*,169.254.* - Доменные исключения:
chatroulett.ru,api.chatroulett.ru,webrtc.chatroulett.ru(для нечисловых значений) - CIDR-диапазоны: private-сети, loopback, link-local, multicast, зарезервированные, подсеть хостинга
6. Механизм наблюдения за интерфейсом
- MutationObserver отслеживает изменения
childList,subtreeи атрибутовdisabled/classнаdocument.documentElement - Автоматически снимает атрибут
disabledс кнопки "ДАЛЬШЕ" - Поиск кнопки по основному CSS-селектору (
button.btn.btn-primary.btn-lg.d-block.w-100.fs-2.px-0.py-3.py-lg-5) и альтернативным селекторам
Визуальный интерфейс
Кнопка блокировки:
- Красная круглая кнопка "BAN" (120×120px), фиксированная позиция в левом верхнем углу
- При клике анализирует все активные WebRTC-соединения, добавляет найденные IP в чёрный список и переключает на следующего собеседника
Системные уведомления:
- Всплывающие сообщения в левом нижнем углу экрана
- Цветовая кодировка:
- Оранжевый — автоматическая блокировка при обнаружении IP из чёрного списка (сообщение:
Блокировка: IP_адрес) - Оранжевый — IP уже находится в чёрном списке (сообщение:
Уже заблокировано: IP1, IP2) - Красный — успешное добавление нового IP в чёрный список (сообщение:
Заблокировано: IP1, IP2) - Серый — IP не найден, соединение ещё не установлено
- Оранжевый — автоматическая блокировка при обнаружении IP из чёрного списка (сообщение:
- Плавное исчезновение через 6 секунд
Пользовательские сценарии:
Автоматическая работа:
- Пользователь заходит в видеочат
- При установке WebRTC-соединения перехватываются SDP и ICE-кандидаты
- Если обнаружен IP из чёрного списка:
- Оранжевое уведомление:
Блокировка: IP_адрес - Мгновенное закрытие соединения (
pc.close()) - Автоматическое нажатие кнопки "ДАЛЬШЕ" через 100 мс
- Поиск нового собеседника
- Оранжевое уведомление:
Ручное управление:
- Пользователь нажимает кнопку "BAN"
- Система анализирует
remoteDescription.sdpвсех активных соединений - Извлекает IP-адреса собеседника (с фильтрацией исключённых)
- Добавляет новые IP в чёрный список и в список последних IP
- Показывает:
Заблокировано: IP1, IP2 - Закрывает все активные соединения
- Автоматически нажимает кнопку "ДАЛЬШЕ"
Критические технические особенности
- Перехват на document-start — RTCPeerConnection патчится до загрузки DOM, гарантируя перехват всех соединений
- Нулевая задержка блокировки — соединения прерываются на этапе SDP/ICE, до установки медиапотока
- Отказоустойчивость — ошибки при анализе соединений перехватываются try/catch без нарушения работы чата
- Автоочистка чёрного списка — при каждом запуске из списка удаляются невалидные и исключённые адреса
- Поддержка IPv6-to-IPv4 трансляции — IPv4-mapped, IPv4-compatible и NAT64 адреса конвертируются в IPv4 для проверки; чистые IPv6 игнорируются
Процесс принятия решения о блокировке
- Извлечение IP-адреса из SDP или ICE-кандидата
- Валидация формата (IPv4 или IPv6)
- Проверка исключений — специальные адреса, домены, CIDR-диапазоны
- Для IPv6 — попытка извлечь IPv4-компонент; при неудаче — пропуск
- Поиск в чёрном списке (localStorage)
- При совпадении — закрытие соединения + автоматический переход к следующему собеседнику
Необходимые требования
Поддерживаемые браузеры:
- Google Chrome
- Mozilla Firefox
- Microsoft Edge
- Opera
- Brave
- Яндекс.Браузер
Требуемое расширение:
- Tampermonkey (рекомендуется)
- Violentmonkey (альтернатива)
Пошаговая инструкция установки
1. Установка менеджера пользовательских скриптов
Для Chrome/Edge/Brave/Яндекс:
- Откройте Chrome Web Store
- Найдите "Tampermonkey"
- Нажмите "Установить"
- Подтвердите установку расширения
Для Firefox:
- Откройте Firefox Add-ons
- Найдите "Tampermonkey"
- Нажмите "Add to Firefox"
- Подтвердите установку
2. Установка скрипта WebRTC Blocker
- Скопируйте полный код скрипта
- Откройте панель Tampermonkey
- Создайте новый скрипт ("Добавить новый скрипт")
- Вставьте код в редактор
- Нажмите "Файл" → "Сохранить"
3. Проверка установки
Признаки успешной установки:
- На странице видеочата появляется красная кнопка "BAN" в левом верхнем углу
- В консоли браузера (F12) видно сообщение:
Chatroulett WebRTC Blocker initialized - Скрипт отображается как активный в панели Tampermonkey
- При блокировке появляются цветные уведомления в левом нижнем углу


Комментарии (338)