Update content-moderation.js

This commit is contained in:
ErikrafT 2025-05-06 20:43:47 -03:00 committed by GitHub
parent 587382a976
commit b6cdb5105d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 378 additions and 159 deletions

View File

@ -93,6 +93,19 @@ class ContentModeration {
'ю': 'yu', // cirílico 'ю' vs latino 'yu'
'я': 'ya' // cirílico 'я' vs latino 'ya'
};
// Inicializa o modelo NSFW
this.nsfwModel = null;
this.initNSFWModel();
}
async initNSFWModel() {
try {
// Carrega o modelo NSFW do TensorFlow.js
this.nsfwModel = await tf.loadGraphModel('https://d1zv2aa70wpiur.cloudfront.net/tfjs_models/tfjs_nsfw_mobilenet/model.json');
} catch (error) {
console.error('Erro ao carregar modelo NSFW:', error);
}
}
// Normaliza o texto removendo substituições de caracteres
@ -124,10 +137,97 @@ class ContentModeration {
async checkNSFW(file) {
if (!this.isMediaFile(file)) return false;
// Aqui você pode integrar com APIs de detecção de conteúdo NSFW
// Por enquanto, vamos usar uma verificação básica de nome de arquivo
const fileName = file.name.toLowerCase();
return this.blockedWords.some(word => fileName.includes(word.toLowerCase()));
try {
// Verifica primeiro pelo nome do arquivo
const fileName = file.name.toLowerCase();
if (this.blockedWords.some(word => fileName.includes(word.toLowerCase()))) {
return true;
}
// Se for uma imagem ou vídeo, verifica o conteúdo
if (file.type.startsWith('image/')) {
return await this.checkImageNSFW(file);
} else if (file.type.startsWith('video/')) {
return await this.checkVideoNSFW(file);
}
return false;
} catch (error) {
console.error('Erro ao verificar NSFW:', error);
return false;
}
}
async checkImageNSFW(file) {
if (!this.nsfwModel) return false;
try {
const img = await createImageBitmap(file);
const tensor = tf.browser.fromPixels(img)
.resizeBilinear([224, 224])
.expandDims()
.toFloat()
.div(255.0);
const predictions = await this.nsfwModel.predict(tensor).data();
tensor.dispose();
// Verifica se o conteúdo é NSFW
const nsfwScore = predictions[1]; // Índice 1 é para conteúdo NSFW
return nsfwScore > 0.5; // Threshold de 50%
} catch (error) {
console.error('Erro ao verificar imagem NSFW:', error);
return false;
}
}
async checkVideoNSFW(file) {
if (!this.nsfwModel) return false;
try {
const video = document.createElement('video');
video.src = URL.createObjectURL(file);
return new Promise((resolve) => {
video.onloadeddata = async () => {
// Captura frames do vídeo para análise
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
// Analisa alguns frames do vídeo
const frameCount = 5;
const interval = video.duration / frameCount;
let nsfwFrames = 0;
for (let i = 0; i < frameCount; i++) {
video.currentTime = i * interval;
await new Promise(r => video.onseeked = r);
ctx.drawImage(video, 0, 0);
const tensor = tf.browser.fromPixels(canvas)
.resizeBilinear([224, 224])
.expandDims()
.toFloat()
.div(255.0);
const predictions = await this.nsfwModel.predict(tensor).data();
tensor.dispose();
if (predictions[1] > 0.5) {
nsfwFrames++;
}
}
URL.revokeObjectURL(video.src);
resolve(nsfwFrames > frameCount / 2); // Se mais da metade dos frames for NSFW
};
});
} catch (error) {
console.error('Erro ao verificar vídeo NSFW:', error);
return false;
}
}
// Verifica se uma URL contém caracteres cirílicos
@ -158,161 +258,208 @@ class ContentModeration {
}
// Verifica se é spam ou contém palavras bloqueadas
isSpam(text) {
// Verifica se é uma URL
const urlMatch = text.match(/^(https?:\/\/[^\s]+)|([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})$/);
if (urlMatch) {
// Se for uma URL, verifica se é suspeita
return this.isSuspiciousUrl(urlMatch[0]);
isSpam(text, file) {
// Verifica se o texto contém palavras bloqueadas
const hasBlockedWords = this.blockedWords.some(word => {
const regex = new RegExp(`\\b${word}\\b`, 'i');
return regex.test(text);
});
// Verifica se o texto contém palavras bloqueadas com substituições
const hasBlockedWordsWithSubs = this.hasBlockedWordsWithSubstitutions(text);
// Verifica se o texto contém URLs suspeitas
const hasSuspiciousUrls = this.isSuspiciousUrl(text);
// Verifica se o texto contém caracteres cirílicos
const hasCyrillicChars = this.hasCyrillicChars(text);
// Verifica se o texto contém emojis impróprios
const hasInappropriateEmojis = this.hasInappropriateEmojis(text);
// Verifica se o texto contém padrões de spam
const hasSpamPatterns = this.spamPatterns.some(pattern => {
if (pattern.type === 'repeated') {
return pattern.regex.test(text);
} else if (pattern.type === 'length') {
return text.length > pattern.threshold;
} else if (pattern.type === 'repetitive') {
return pattern.regex.test(text);
}
return false;
});
// Determina o tipo de conteúdo impróprio
let contentType = 'spam';
if (hasBlockedWords || hasBlockedWordsWithSubs) {
contentType = 'profanity';
}
if (hasInappropriateEmojis || this.isExplicitContent(text)) {
contentType = 'explicit';
}
if (hasSuspiciousUrls || hasCyrillicChars) {
contentType = 'scam';
}
// Verifica palavras bloqueadas com substituições
if (this.hasBlockedWordsWithSubstitutions(text)) {
return true;
}
// Verifica padrões de spam
return this.spamPatterns.some(pattern => pattern.test(text));
// Retorna o resultado com o tipo de conteúdo
return {
isSpam: hasBlockedWords || hasBlockedWordsWithSubs || hasSuspiciousUrls ||
hasCyrillicChars || hasInappropriateEmojis || hasSpamPatterns,
contentType: contentType
};
}
// Método para verificar emojis impróprios
hasInappropriateEmojis(text) {
const inappropriateEmojis = ['🔞', '🍆', '🍑', '🥒', '🥵', '💦', '👅', '👙', '👄', '💋'];
return inappropriateEmojis.some(emoji => text.includes(emoji));
}
// Método para verificar conteúdo explícito
isExplicitContent(text) {
const explicitTerms = ['porn', 'sex', 'nude', 'nudes', 'onlyfans', 'leaks', 'hentai'];
return explicitTerms.some(term => text.toLowerCase().includes(term));
}
// Mostra o diálogo de aviso
showWarningDialog(file, contentType = 'explicit') {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'content-warning-dialog';
// Define o ícone e mensagem baseado no tipo de conteúdo
let iconSvg = '';
let warningTitle = '';
let warningMessage = '';
showWarningDialog(file, contentType = 'spam') {
const dialog = document.createElement('div');
dialog.className = 'content-warning-dialog';
switch(contentType) {
case 'explicit':
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 -960 960 960" width="64" fill="#e3e3e3">
<path d="M764-84 624-222q-35 11-71 16.5t-73 5.5q-134 0-245-72T61-462q-5-9-7.5-18.5T51-500q0-10 2.5-19.5T61-538q22-39 47-76t58-66l-83-84q-11-11-11-27.5T84-820q11-11 28-11t28 11l680 680q11 11 11.5 27.5T820-84q-11 11-28 11t-28-11ZM480-320q11 0 21-1t20-4L305-541q-3 10-4 20t-1 21q0 75 52.5 127.5T480-320Zm0-480q134 0 245.5 72.5T900-537q5 8 7.5 17.5T910-500q0 10-2 19.5t-7 17.5q-19 37-42.5 70T806-331q-14 14-33 13t-33-15l-80-80q-7-7-9-16.5t1-19.5q4-13 6-25t2-26q0-75-52.5-127.5T480-680q-14 0-26 2t-25 6q-10 3-20 1t-17-9l-33-33q-19-19-12.5-44t31.5-32q25-5 50.5-8t51.5-3Zm79 226q11 13 18.5 28.5T587-513q1 8-6 11t-13-3l-82-82q-6-6-2.5-13t11.5-7q19 2 35 10.5t29 22.5Z"/>
</svg>`;
warningTitle = 'Conteúdo Explícito';
warningMessage = 'Este conteúdo pode conter material adulto ou impróprio';
break;
// Define o ícone e mensagem baseado no tipo de conteúdo
let icon, title, message;
switch (contentType) {
case 'explicit':
icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
<circle cx="12" cy="12" r="3"/>
<line x1="3" y1="3" x2="21" y2="21"/>
</svg>`;
title = 'Conteúdo Explícito';
message = 'Este conteúdo pode conter material adulto ou impróprio';
break;
case 'profanity':
icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="4.93" y1="19.07" x2="19.07" y2="4.93"/>
</svg>`;
title = 'Linguagem Imprópria';
message = 'Este conteúdo contém linguagem ofensiva ou inadequada';
break;
case 'scam':
icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`;
title = 'Possível Golpe';
message = 'Este conteúdo pode ser uma tentativa de golpe ou fraude';
break;
default:
icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>`;
title = 'Possível Spam';
message = 'Este conteúdo pode ser spam ou tentativa de golpe';
}
case 'profanity':
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 -960 960 960" width="64" fill="#e3e3e3">
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q54 0 104-17.5t92-50.5L228-676q-33 42-50.5 92T160-480q0 134 93 227t227 93Zm252-124q33-42 50.5-92T800-480q0-134-93-227t-227-93q-54 0-104 17.5T284-732l448 448Z"/>
</svg>`;
warningTitle = 'Linguagem Imprópria';
warningMessage = 'Este conteúdo contém linguagem ofensiva ou inadequada';
break;
case 'spam':
iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 -960 960 960" width="64" fill="#e3e3e3">
<path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm0-160q17 0 28.5-11.5T520-480v-160q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640v160q0 17 11.5 28.5T480-440ZM363-120q-16 0-30.5-6T307-143L143-307q-11-11-17-25.5t-6-30.5v-234q0-16 6-30.5t17-25.5l164-164q11-11 25.5-17t30.5-6h234q16 0 30.5 6t25.5 17l164 164q11 11 17 25.5t6 30.5v234q0 16-6 30.5T817-307L653-143q-11 11-25.5 17t-30.5 6H363Z"/>
</svg>`;
warningTitle = 'Possível Spam';
warningMessage = 'Este conteúdo pode ser spam ou tentativa de golpe';
break;
}
// Cria um preview da imagem/vídeo com desfoque
let mediaPreview = '';
if (this.isMediaFile(file)) {
const objectUrl = URL.createObjectURL(file);
if (file.type.startsWith('image/')) {
mediaPreview = `
<div class="warning-icon">
${iconSvg}
</div>
<div class="warning-text">
<p>${warningTitle}</p>
<p>${warningMessage}</p>
</div>
<div class="media-preview blurred">
<img src="${objectUrl}" alt="Preview">
</div>`;
} else if (file.type.startsWith('video/')) {
mediaPreview = `
<div class="warning-icon">
${iconSvg}
</div>
<div class="warning-text">
<p>${warningTitle}</p>
<p>${warningMessage}</p>
</div>
<div class="media-preview blurred">
<video src="${objectUrl}" muted></video>
</div>`;
const content = document.createElement('div');
content.className = 'warning-content';
content.innerHTML = `
<div class="warning-icon">
${icon}
</div>
<div class="warning-text">
<p>${title}</p>
<p>${message}</p>
</div>
<div class="media-preview blurred">
${file.type.startsWith('image/') ?
`<img src="${URL.createObjectURL(file)}" alt="Preview">` :
`<video src="${URL.createObjectURL(file)}" muted></video>`
}
} else {
// Para conteúdo que não é mídia (texto, etc)
mediaPreview = `
<div class="warning-icon">
${iconSvg}
</div>
<div class="warning-text">
<p>${warningTitle}</p>
<p>${warningMessage}</p>
</div>`;
</div>
<div class="warning-buttons">
<button class="btn-cancel">Recusar</button>
<button class="btn-view">Ver</button>
</div>
`;
dialog.appendChild(content);
document.body.appendChild(dialog);
// Adiciona eventos aos botões
const cancelBtn = content.querySelector('.btn-cancel');
const viewBtn = content.querySelector('.btn-view');
const mediaPreview = content.querySelector('.media-preview');
cancelBtn.onclick = () => {
dialog.remove();
URL.revokeObjectURL(mediaPreview.querySelector('img, video').src);
};
viewBtn.onclick = () => {
mediaPreview.classList.remove('blurred');
viewBtn.style.display = 'none';
cancelBtn.textContent = 'Fechar';
};
// Adiciona evento para fechar com ESC
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
dialog.remove();
URL.revokeObjectURL(mediaPreview.querySelector('img, video').src);
document.removeEventListener('keydown', escHandler);
}
dialog.innerHTML = `
<div class="warning-content">
${mediaPreview}
<div class="warning-buttons">
<button class="btn-cancel">Recusar</button>
<button class="btn-view">Ver</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// Adiciona evento para remover o URL do objeto quando o diálogo for fechado
const cleanup = () => {
if (this.isMediaFile(file)) {
URL.revokeObjectURL(objectUrl);
}
};
dialog.querySelector('.btn-cancel').onclick = () => {
cleanup();
document.body.removeChild(dialog);
resolve(false);
};
dialog.querySelector('.btn-view').onclick = () => {
cleanup();
document.body.removeChild(dialog);
resolve(true);
};
});
}
// Processa um arquivo antes de enviar
async processFile(file) {
// Verifica spam no nome do arquivo
if (this.isSpam(file.name)) {
const shouldView = await this.showWarningDialog(file, 'spam');
if (!shouldView) {
throw new Error('Arquivo bloqueado: Possível spam detectado');
}
}
try {
// Verifica o arquivo no servidor proxy
const formData = new FormData();
formData.append('file', file);
// Verifica conteúdo NSFW
if (await this.checkNSFW(file)) {
const shouldView = await this.showWarningDialog(file, 'explicit');
if (!shouldView) {
throw new Error('Arquivo bloqueado: Conteúdo impróprio');
}
}
const response = await fetch('http://localhost:3001/check-content', {
method: 'POST',
body: formData
});
// Verifica linguagem imprópria
if (this.hasBlockedWordsWithSubstitutions(file.name)) {
const shouldView = await this.showWarningDialog(file, 'profanity');
if (!shouldView) {
throw new Error('Arquivo bloqueado: Linguagem imprópria');
}
}
const result = await response.json();
return file;
if (result.blocked) {
const shouldView = await this.showWarningDialog(file, 'explicit');
if (!shouldView) {
throw new Error(`Arquivo bloqueado: ${result.reason}`);
}
}
return file;
} catch (error) {
console.error('Erro ao processar arquivo:', error);
throw error;
}
}
async checkUrl(url) {
try {
const response = await fetch('http://localhost:3001/check-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url })
});
const result = await response.json();
return result.blocked;
} catch (error) {
console.error('Erro ao verificar URL:', error);
return false;
}
}
}
@ -325,64 +472,105 @@ style.textContent = `
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
background: rgba(0, 0, 0, 0.95);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(10px);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.warning-content {
background: #1a1a1a;
padding: 30px;
border-radius: 12px;
border-radius: 16px;
max-width: 500px;
text-align: center;
color: white;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
animation: slideUp 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.warning-icon {
width: 64px;
height: 64px;
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
animation: pulse 2s infinite;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
@keyframes pulse {
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.05); }
100% { transform: translate(-50%, -50%) scale(1); }
}
.warning-icon svg {
filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.5));
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.3));
width: 64px;
height: 64px;
}
.warning-text {
text-align: center;
margin-bottom: 20px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 100%;
padding: 0 20px;
}
.warning-text p:first-child {
font-size: 24px;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
color: #ff4444;
background: linear-gradient(45deg, #ff4444, #ff0000);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.warning-text p:last-child {
font-size: 16px;
font-size: 18px;
color: #e3e3e3;
line-height: 1.5;
}
.media-preview {
position: relative;
width: 100%;
max-height: 300px;
border-radius: 8px;
border-radius: 12px;
overflow: hidden;
margin-top: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.media-preview img,
@ -390,11 +578,12 @@ style.textContent = `
width: 100%;
height: 100%;
object-fit: cover;
transition: filter 0.3s ease;
}
.blurred img,
.blurred video {
filter: blur(20px);
filter: blur(25px) brightness(0.7);
}
.warning-buttons {
@ -403,36 +592,66 @@ style.textContent = `
gap: 20px;
margin-top: 20px;
width: 100%;
position: absolute;
bottom: 30px;
left: 0;
z-index: 2;
}
.warning-buttons button {
padding: 12px 24px;
padding: 14px 28px;
border: none;
border-radius: 6px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease;
flex: 1;
max-width: 200px;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-cancel {
background: #ff4444;
background: linear-gradient(45deg, #ff4444, #ff0000);
color: white;
box-shadow: 0 4px 15px rgba(255, 0, 0, 0.3);
}
.btn-cancel:hover {
background: #ff0000;
background: linear-gradient(45deg, #ff0000, #cc0000);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 0, 0, 0.4);
}
.btn-view {
background: #4CAF50;
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.btn-view:hover {
background: #45a049;
background: linear-gradient(45deg, #45a049, #388e3c);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}
/* Estilo para notificações de golpe */
.scam-warning {
background: linear-gradient(45deg, #ff6b6b, #ff0000);
color: white;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
font-weight: bold;
text-align: center;
animation: shake 0.5s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
`;
document.head.appendChild(style);