PairDrop/public/scripts/content-moderation.js

1198 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Sistema de Moderação de Conteúdo
class ContentModeration {
constructor() {
this.blockedWords = [
// URLs e Links suspeitos
'.gg/HpZzvY5W', '.gg/TZsbat4tw6', 'discord.gg/doncommunity', 'discord.gg/P93HBWRp',
'bit.ly', 'encurtador.com.br', 'is.gd', 'kurl.ru', 'l1nk.dev',
'n9.cl', 'rb.gy', 'shorturl.at', 'shre.ink', 'surl.li', 't.ly', 't.me', 'tinyurl.com',
'u.to', 'urlzs.com', 'zzb.bz', 'steamcommunity', 'steamgift', 'steamrconmmunity',
'steamscommunuty', 'steamshort', 'steanmecomnmunity', 'store-steaempowered',
'share.blood-strike.com', 'casumonster.top', 'abre.ai', 'abrir.link', 'open.ai', 'open.link',
// Palavrões e termos ofensivos
'arromb', 'asshole', 'babac', 'bastard', 'bct', 'boceta', 'bocetas', 'boquete', 'bosta',
'bostinha', 'buceta', 'bucetas', 'burro', 'cacete', 'caralh', 'caralho', 'corno', 'corna',
'crlh', 'cu', 'cuckold', 'cum', 'cumshot', 'cunt', 'cunts', 'cuz', 'desgraça', 'desgraçado',
'dick', 'escrot', 'fdp', 'foda', 'fuck', 'gay', 'idiota', 'imbecil', 'merda', 'otario',
'otário', 'pau', 'pinto', 'porra', 'puta', 'putas', 'puto', 'quenga', 'quengo', 'retardado',
'safado', 'shit', 'shitty', 'viad', 'viado', 'xereca', 'xoxota', 'xvideos', 'xxxvideos',
// Novos termos ofensivos
'arrobado', 'vadia', 'vadio', 'vagabunda', 'vagabundo', 'piranha', 'prostituta', 'prostituto',
'putinha', 'putinho', 'viadinho', 'viadinha', 'bocetinha', 'bucetinha', 'cuzinho', 'cuzinha',
'caralhinho', 'caralhinha', 'pauzinho', 'pauzinha', 'pintinho', 'pintinha', 'merdinha',
'merdinho', 'bostinha', 'bostinho', 'fodinha', 'fodinho', 'putinha', 'putinho',
// Termos NSFW e conteúdo adulto
'🔞', '🍆', '🍑', '🥒', '🥵', 'PORN', 'Pornografia', 'pornografía', 'pornography',
'nude', 'nudes', 'Onlyfans', 'OnlyFans', 'Leaks', 'Hentai', 'Teen Porn', 'E-Girls Porn',
'Latina Nudes', 'xnudes', 'xvideos', 'pornhub', 'xhamster', 'redtube', 'sexy', 'sexy girl',
'sexo', 'sex',
// Spam e golpes
'$100', '$20 gift', '20$ gift', 'Bilhão de reais', 'Billion Dollars', 'Billion of reais',
'Billion reais', 'buy now', 'cheap', 'click here', 'follow me', 'free gift', 'free nudes',
'gift from steam', 'gifts', 'here', 'Hot', 'HOT', 'prize', 'vote for me',
'withdrew $15647', 'Milhão de reais', 'Million Dollars', 'Million reais',
// Outros termos ofensivos
'dirty black', 'negro sujo', 'worm', 'verme', 'trash', 'worthless', 'idiot', 'imbecile',
'shut up', 'cala a boca'
];
// Termos explícitos
this.explicitTerms = [
'porn', 'sex', 'xxx', 'adult', 'nude', 'naked', 'nsfw', '18+',
'pornografia', 'sexo', 'adulto', 'nu', 'nua', 'nudez', 'erótico',
'onlyfans', 'leaks', 'hentai', 'pussy', 'buceta', 'xereca', 'xereka',
'chereca', 'hentai', 'pornhub', 'xhamster', 'redtube', 'sexy', 'sexy girl',
'sexo', 'sex', 'porn', 'pornografia', 'erótico', 'erótica',
];
// Termos ofensivos
this.offensiveTerms = [
'arromb', 'asshole', 'babac', 'bastard', 'bct', 'boceta', 'boquete',
'burro', 'cacete', 'caralh', 'corno', 'corna', 'crlh', 'cu', 'puta'
];
// Termos de golpe
this.scamTerms = [
'hack', 'crack', 'pirata', 'gratis', 'free', 'win', 'premio', 'prêmio',
'ganhou', 'bitcoin', 'crypto', 'investment', 'investimento', 'money'
];
this.spamPatterns = [
/(.)\1{10,}/, // Caracteres repetidos (aumentado para 10+ repetições)
/(.){1000,}/, // Textos muito longos (aumentado para 1000+ caracteres)
/(.){1,10}\1{5,}/, // Padrões repetitivos
/(.){1,5}\1{10,}/, // Caracteres repetidos em sequência
/(.){1,3}\1{15,}/ // Caracteres muito repetidos
];
// Mapeamento de substituições comuns
this.characterSubstitutions = {
'a': ['4', '@', 'α', 'а'],
'o': ['0', '@', 'о', 'ο'],
'e': ['3', 'ε', 'е'],
'i': ['1', '!', 'і', 'ι'],
's': ['$', '5', 'ѕ'],
't': ['7', 'т'],
'b': ['8', 'в'],
'g': ['9', 'ɡ'],
'l': ['1', '|', 'ł'],
'z': ['2', 'з']
};
// Caracteres cirílicos que podem ser usados para enganar
this.cyrillicChars = {
'а': 'a', // cirílico 'а' vs latino 'a'
'е': 'e', // cirílico 'е' vs latino 'e'
'о': 'o', // cirílico 'о' vs latino 'o'
'с': 'c', // cirílico 'с' vs latino 'c'
'р': 'p', // cirílico 'р' vs latino 'p'
'у': 'y', // cirílico 'у' vs latino 'y'
'х': 'x', // cirílico 'х' vs latino 'x'
'і': 'i', // cirílico 'і' vs latino 'i'
'ѕ': 's', // cirílico 'ѕ' vs latino 's'
'т': 't', // cirílico 'т' vs latino 't'
'в': 'b', // cirílico 'в' vs latino 'b'
'ɡ': 'g', // cirílico 'ɡ' vs latino 'g'
'з': 'z', // cirílico 'з' vs latino 'z'
'м': 'm', // cirílico 'м' vs latino 'm'
'н': 'h', // cirílico 'н' vs latino 'h'
'к': 'k', // cirílico 'к' vs latino 'k'
'л': 'l', // cirílico 'л' vs latino 'l'
'д': 'd', // cirílico 'д' vs latino 'd'
'ф': 'f', // cirílico 'ф' vs latino 'f'
'ц': 'c', // cirílico 'ц' vs latino 'c'
'ч': 'ch', // cirílico 'ч' vs latino 'ch'
'ш': 'sh', // cirílico 'ш' vs latino 'sh'
'щ': 'sch', // cirílico 'щ' vs latino 'sch'
'ъ': '', // cirílico 'ъ' (não tem equivalente)
'ь': '', // cirílico 'ь' (não tem equivalente)
'ю': 'yu', // cirílico 'ю' vs latino 'yu'
'я': 'ya' // cirílico 'я' vs latino 'ya'
};
// Múltiplos modelos NSFW
this.nsfwModels = {
default: null,
mobilenet: null,
inception: null,
resnet: null
};
// Status de carregamento dos modelos
this.modelLoading = false;
// URLs dos modelos
this.modelUrls = {
default: 'https://cdn.jsdelivr.net/npm/nsfwjs@2.4.0/dist/model/',
mobilenet: 'https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@2.1.0/dist/model.json',
inception: 'https://storage.googleapis.com/tfjs-models/tfjs/inception_v3/2/model.json',
resnet: 'https://cdn.jsdelivr.net/npm/@tensorflow-models/toxicity@1.2.2/dist/model.json'
};
// APIs externas de moderação
this.externalApis = {
deepai: 'https://api.deepai.org/api/nsfw-detector',
sightengine: 'https://api.sightengine.com/1.0/check.json',
moderatecontent: 'https://api.moderatecontent.com/moderate/',
imagga: 'https://api.imagga.com/v2/categories/nsfw_beta',
cloudmersive: 'https://api.cloudmersive.com/image/nsfw/classify'
};
// Inicializa todos os modelos
this.loadAllModels();
}
async loadAllModels() {
if (this.modelLoading) return;
this.modelLoading = true;
try {
console.log('Carregando múltiplos modelos NSFW...');
// Carrega modelo principal do NSFWJS
this.nsfwModels.default = await nsfwjs.load(this.modelUrls.default);
console.log('Modelo NSFWJS principal carregado');
// Carrega MobileNet para detecção adicional
this.nsfwModels.mobilenet = await tf.loadLayersModel(this.modelUrls.mobilenet);
console.log('Modelo MobileNet carregado');
// Carrega Inception para classificação avançada
this.nsfwModels.inception = await tf.loadLayersModel(this.modelUrls.inception);
console.log('Modelo Inception carregado');
// Carrega ResNet para detecção de características
this.nsfwModels.resnet = await tf.loadLayersModel(this.modelUrls.resnet);
console.log('Modelo ResNet carregado');
} catch (error) {
console.error('Erro ao carregar modelos:', error);
}
this.modelLoading = false;
}
// Normaliza o texto removendo substituições de caracteres
normalizeText(text) {
let normalized = text.toLowerCase();
// Substitui caracteres especiais de volta para suas formas normais
for (const [normal, substitutes] of Object.entries(this.characterSubstitutions)) {
for (const substitute of substitutes) {
normalized = normalized.replace(new RegExp(substitute, 'g'), normal);
}
}
return normalized;
}
// Verifica se o texto contém palavras bloqueadas mesmo com substituições
hasBlockedWordsWithSubstitutions(text) {
const normalized = this.normalizeText(text);
return this.blockedWords.some(word => normalized.includes(word.toLowerCase()));
}
// Verifica se o arquivo é uma imagem ou vídeo
isMediaFile(file) {
return file.type.startsWith('image/') || file.type.startsWith('video/');
}
// Verifica se o conteúdo é NSFW
async checkNSFW(file) {
if (!this.isMediaFile(file)) return false;
try {
console.log('Iniciando verificação NSFW completa para:', file.name);
// Verifica o nome do arquivo primeiro
const fileName = file.name.toLowerCase();
if (this.blockedWords.some(word => fileName.includes(word.toLowerCase()))) {
console.log('Nome do arquivo contém palavras bloqueadas');
return {
isNSFW: true,
confidence: 1.0,
reason: 'Nome do arquivo bloqueado'
};
}
// Verifica metadados do arquivo
if (file.type.includes('image') || file.type.includes('video')) {
const metadata = await this.extractMetadata(file);
if (metadata.isNSFW) {
return {
isNSFW: true,
confidence: metadata.confidence,
reason: 'Metadados suspeitos'
};
}
}
// Garante que os modelos estão carregados
if (!this.nsfwModels.default) {
await this.loadAllModels();
}
let isNSFW = false;
let confidence = 0;
let modelResults = {};
if (file.type.startsWith('image/')) {
const results = await this.checkImageWithAllModels(file);
isNSFW = results.isNSFW;
confidence = results.confidence;
modelResults = results.modelResults;
} else if (file.type.startsWith('video/')) {
const results = await this.checkVideoWithAllModels(file);
isNSFW = results.isNSFW;
confidence = results.confidence;
modelResults = results.modelResults;
}
// Verifica APIs externas se necessário
if (confidence > 0.1) { // Se houver alguma suspeita
const externalResults = await this.checkExternalApis(file);
if (externalResults.isNSFW) {
isNSFW = true;
confidence = Math.max(confidence, externalResults.confidence);
}
}
// Se o conteúdo for NSFW, aplica blur automaticamente
if (isNSFW) {
const element = document.querySelector(`[data-file-id="${file.name}"]`);
if (element) {
this.applyBlurAndOverlay(element, 'explicit');
}
}
return {
isNSFW,
confidence,
modelResults,
fileType: file.type.startsWith('image/') ? 'image' : 'video'
};
} catch (error) {
console.error('Erro na verificação NSFW:', error);
return {
isNSFW: false,
confidence: 0,
modelResults: {},
error: error.message
};
}
}
// Extrai metadados do arquivo
async extractMetadata(file) {
return new Promise((resolve) => {
if (file.type.startsWith('image/')) {
const img = new Image();
img.onload = () => {
// Verifica dimensões suspeitas
const isSuspiciousSize = img.width > 2000 || img.height > 2000;
// Verifica proporção suspeita
const ratio = img.width / img.height;
const isSuspiciousRatio = ratio < 0.5 || ratio > 2;
resolve({
isNSFW: isSuspiciousSize || isSuspiciousRatio,
confidence: (isSuspiciousSize || isSuspiciousRatio) ? 0.3 : 0,
width: img.width,
height: img.height,
ratio: ratio
});
};
img.src = URL.createObjectURL(file);
} else {
resolve({ isNSFW: false, confidence: 0 });
}
});
}
// Verifica APIs externas
async checkExternalApis(file) {
try {
const formData = new FormData();
formData.append('file', file);
// Tenta cada API em sequência
for (const [name, url] of Object.entries(this.externalApis)) {
try {
const response = await fetch(url, {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
if (result.isNSFW || result.nsfw_score > 0.15) {
return {
isNSFW: true,
confidence: result.nsfw_score || 0.5,
api: name
};
}
}
} catch (error) {
console.error(`Erro na API ${name}:`, error);
}
}
return { isNSFW: false, confidence: 0 };
} catch (error) {
console.error('Erro ao verificar APIs externas:', error);
return { isNSFW: false, confidence: 0 };
}
}
async checkImageWithAllModels(file) {
return new Promise(async (resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = async () => {
try {
console.log('Analisando imagem com múltiplos modelos...');
// Resultados de cada modelo
const modelResults = {
nsfwjs: null,
mobilenet: null,
inception: null,
resnet: null
};
// NSFWJS
const nsfwPredictions = await this.nsfwModels.default.classify(img);
modelResults.nsfwjs = nsfwPredictions;
// MobileNet
const mobilenetPredictions = await this.nsfwModels.mobilenet.classify(img);
modelResults.mobilenet = mobilenetPredictions;
// Inception
const inceptionPredictions = await this.nsfwModels.inception.classify(img);
modelResults.inception = inceptionPredictions;
// ResNet
const resnetPredictions = await this.nsfwModels.resnet.classify(img);
modelResults.resnet = resnetPredictions;
// Análise ponderada dos resultados
let nsfwScore = 0;
let totalConfidence = 0;
// NSFWJS (peso 2)
const nsfwjsScore = nsfwPredictions.find(p => p.className === 'Porn' || p.className === 'Hentai');
if (nsfwjsScore) {
nsfwScore += nsfwjsScore.probability * 2;
totalConfidence += 2;
}
// MobileNet (peso 1)
const mobilenetScore = mobilenetPredictions.find(p => p.className.toLowerCase().includes('explicit'));
if (mobilenetScore) {
nsfwScore += mobilenetScore.probability;
totalConfidence += 1;
}
// Inception (peso 1.5)
const inceptionScore = inceptionPredictions.find(p => p.className.toLowerCase().includes('adult'));
if (inceptionScore) {
nsfwScore += inceptionScore.probability * 1.5;
totalConfidence += 1.5;
}
// ResNet (peso 1.5)
const resnetScore = resnetPredictions.find(p => p.className.toLowerCase().includes('nsfw'));
if (resnetScore) {
nsfwScore += resnetScore.probability * 1.5;
totalConfidence += 1.5;
}
// Calcula a média ponderada
const finalScore = nsfwScore / totalConfidence;
const isNSFW = finalScore > 0.15; // Threshold reduzido para 15%
console.log('Resultado final NSFW:', {
isNSFW,
confidence: finalScore,
modelResults
});
resolve({
isNSFW,
confidence: finalScore,
modelResults
});
} catch (error) {
console.error('Erro ao analisar imagem:', error);
resolve({
isNSFW: false,
confidence: 0,
modelResults: {},
error: error.message
});
}
};
img.onerror = () => {
console.error('Erro ao carregar imagem para análise');
resolve({
isNSFW: false,
confidence: 0,
modelResults: {},
error: 'Erro ao carregar imagem'
});
};
img.src = URL.createObjectURL(file);
});
}
async checkVideoWithAllModels(file) {
return new Promise((resolve) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.muted = true;
video.onloadedmetadata = async () => {
try {
console.log('Analisando vídeo com múltiplos modelos...');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Configurações para análise de frames
const frameInterval = 1000; // 1 frame por segundo
const maxFrames = Math.min(10, Math.floor(video.duration)); // Máximo 10 frames
let framesAnalyzed = 0;
let totalNSFWScore = 0;
// Array para armazenar resultados de cada frame
const frameResults = [];
// Função para analisar um frame específico
const analyzeFrame = async (time) => {
video.currentTime = time;
await new Promise(resolve => video.onseeked = resolve);
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Análise com múltiplos modelos
const [nsfwResults, mobilenetResults, inceptionResults] = await Promise.all([
this.nsfwModels.default.classify(canvas),
this.nsfwModels.mobilenet.classify(canvas),
this.nsfwModels.inception.classify(canvas)
]);
// Calcula score NSFW para o frame
let frameScore = 0;
let totalWeight = 0;
// NSFWJS (peso 2)
const nsfwScore = nsfwResults.find(p => p.className === 'Porn' || p.className === 'Hentai');
if (nsfwScore) {
frameScore += nsfwScore.probability * 2;
totalWeight += 2;
}
// MobileNet (peso 1)
const mobilenetScore = mobilenetResults.find(p => p.className.toLowerCase().includes('explicit'));
if (mobilenetScore) {
frameScore += mobilenetScore.probability;
totalWeight += 1;
}
// Inception (peso 1.5)
const inceptionScore = inceptionResults.find(p => p.className.toLowerCase().includes('adult'));
if (inceptionScore) {
frameScore += inceptionScore.probability * 1.5;
totalWeight += 1.5;
}
return {
time,
score: frameScore / totalWeight,
results: {
nsfwjs: nsfwResults,
mobilenet: mobilenetResults,
inception: inceptionResults
}
};
};
// Analisa frames em intervalos regulares
for (let i = 0; i < maxFrames; i++) {
const time = i * (video.duration / maxFrames);
const frameResult = await analyzeFrame(time);
frameResults.push(frameResult);
totalNSFWScore += frameResult.score;
framesAnalyzed++;
}
// Calcula média final
const averageScore = totalNSFWScore / framesAnalyzed;
const isNSFW = averageScore > 0.15; // Threshold reduzido para 15%
console.log('Resultado final vídeo:', {
isNSFW,
confidence: averageScore,
frameResults
});
resolve({
isNSFW,
confidence: averageScore,
modelResults: frameResults
});
} catch (error) {
console.error('Erro ao analisar vídeo:', error);
resolve({
isNSFW: false,
confidence: 0,
modelResults: {},
error: error.message
});
}
};
video.onerror = () => {
console.error('Erro ao carregar vídeo para análise');
resolve({
isNSFW: false,
confidence: 0,
modelResults: {},
error: 'Erro ao carregar vídeo'
});
};
video.src = URL.createObjectURL(file);
});
}
// Verifica se uma URL contém caracteres cirílicos
hasCyrillicChars(url) {
const cyrillicPattern = /[\u0400-\u04FF]/;
return cyrillicPattern.test(url);
}
// Normaliza uma URL removendo caracteres cirílicos
normalizeUrl(url) {
let normalized = url.toLowerCase();
for (const [cyrillic, latin] of Object.entries(this.cyrillicChars)) {
normalized = normalized.replace(new RegExp(cyrillic, 'g'), latin);
}
return normalized;
}
// Verifica se uma URL é suspeita
isSuspiciousUrl(url) {
// Verifica se contém caracteres cirílicos
if (this.hasCyrillicChars(url)) {
return true;
}
// Verifica se a URL normalizada contém palavras bloqueadas
const normalizedUrl = this.normalizeUrl(url);
return this.blockedWords.some(word => normalizedUrl.includes(word.toLowerCase()));
}
// Verifica se é spam ou contém palavras bloqueadas
isSpam(text, file) {
if (!text) return { isSpam: false, contentType: null };
// Normaliza o texto para comparação
const normalizedText = this.normalizeText(text.toLowerCase());
// Sistema de pontuação para determinar o tipo de conteúdo
let spamScore = 0;
let offensiveScore = 0;
let explicitScore = 0;
let scamScore = 0;
// Verifica palavras bloqueadas com sistema de pontuação
for (const word of this.blockedWords) {
const regex = new RegExp(`\\b${word}\\b`, 'i');
if (regex.test(normalizedText)) {
if (this.explicitTerms.includes(word)) explicitScore += 2;
else if (this.offensiveTerms.includes(word)) offensiveScore += 2;
else if (this.scamTerms.includes(word)) scamScore += 2;
else spamScore += 1;
}
}
// Verifica padrões de spam
if (/(.)\\1{4,}/.test(normalizedText)) spamScore += 2; // Caracteres repetidos
if (text.length > 500) spamScore += 1; // Mensagens muito longas
if ((text.match(/[A-Z]/g) || []).length > text.length * 0.7) spamScore += 2; // Muitas maiúsculas
// Verifica URLs suspeitas
const urlRegex = /https?:\/\/[^\s]+/g;
const urls = text.match(urlRegex) || [];
for (const url of urls) {
if (this.isSuspiciousUrl(url)) {
scamScore += 3;
}
}
// Verifica emojis impróprios
if (this.hasInappropriateEmojis(text)) {
explicitScore += 2;
}
// Determina o tipo de conteúdo baseado nos scores
let contentType = null;
let isSpam = false;
if (explicitScore >= 2) {
contentType = 'explicit';
isSpam = true;
} else if (scamScore >= 3) {
contentType = 'scam';
isSpam = true;
} else if (offensiveScore >= 2) {
contentType = 'offensive';
isSpam = true;
} else if (spamScore >= 3) {
contentType = 'spam';
isSpam = true;
}
return {
isSpam,
contentType,
scores: {
explicit: explicitScore,
scam: scamScore,
offensive: offensiveScore,
spam: spamScore
}
};
}
// 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
async showWarningDialog(file, contentType = 'spam') {
return new Promise((resolve) => {
const dialog = document.createElement('div');
dialog.className = 'content-moderation-warning';
let title, message, icon;
switch(contentType) {
case 'explicit':
title = '🚫 Conteúdo Explícito Detectado';
message = 'Este conteúdo pode conter material explícito ou inadequado.';
icon = `<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 -960 960 960" width="64" fill="#ff4444">
<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>`;
break;
case 'spam':
title = '🚫 Possível Spam/Golpe Detectado';
message = 'Este conteúdo pode ser spam ou tentativa de golpe.';
icon = `<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 -960 960 960" width="64" fill="#ff4444">
<path d="M109-120q-11 0-20-5.5T75-140q-5-9-5.5-19.5T75-180l370-640q6-10 15.5-15t19.5-5q10 0 19.5 5t15.5 15l370 640q6 10 5.5 20.5T885-140q-5 9-14 14.5t-20 5.5H109Zm371-120q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm0-120q17 0 28.5-11.5T520-400v-120q0-17-11.5-28.5T480-560q-17 0-28.5 11.5T440-520v120q0 17 11.5 28.5T480-360Z"/>
</svg>`;
break;
case 'offensive':
title = '🚫 Conteúdo Ofensivo Detectado';
message = 'Este conteúdo contém linguagem ofensiva.';
icon = `<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 -960 960 960" width="64" fill="#ff4444">
<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>`;
break;
}
dialog.innerHTML = `
<div class="warning-content">
<div class="warning-icon" data-type="${contentType}">${icon}</div>
<div class="warning-title" data-type="${contentType}">${title}</div>
<div class="warning-message">${message}</div>
<div class="warning-preview blurred">
${file.type.startsWith('image/') ?
`<img src="${URL.createObjectURL(file)}" alt="Preview">` :
file.type.startsWith('video/') ?
`<video src="${URL.createObjectURL(file)}" controls></video>` :
`<div class="file-info">${file.name}</div>`
}
</div>
<div class="warning-notice" data-type="${contentType}">
<p>⚠️ Este conteúdo foi identificado como potencialmente perigoso.</p>
</div>
<div class="warning-buttons">
<button class="warning-button show">Mostrar Conteúdo</button>
<button class="warning-button close">Fechar</button>
<button class="warning-button block">Sempre Recusar Conteúdos Perigosos</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const showButton = dialog.querySelector('.warning-button.show');
const closeButton = dialog.querySelector('.warning-button.close');
const blockButton = dialog.querySelector('.warning-button.block');
const preview = dialog.querySelector('.warning-preview');
showButton.onclick = () => {
preview.classList.remove('blurred');
showButton.style.display = 'none';
};
closeButton.onclick = () => {
document.body.removeChild(dialog);
resolve(false);
};
blockButton.onclick = () => {
localStorage.setItem('blockDangerousContent', 'true');
document.body.removeChild(dialog);
resolve(false);
};
// Fecha o diálogo com ESC
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
document.body.removeChild(dialog);
document.removeEventListener('keydown', escHandler);
resolve(false);
}
});
});
}
// Aplica blur e overlay em conteúdo sensível
applyBlurAndOverlay(element, contentType) {
element.classList.add('blurred-content');
const overlay = document.createElement('div');
overlay.className = 'warning-overlay';
let icon, text;
switch(contentType) {
case 'explicit':
icon = `<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#ffdd00">
<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>`;
text = 'Conteúdo Explícito Detectado';
break;
case 'spam':
icon = `<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#ff0000">
<path d="M109-120q-11 0-20-5.5T75-140q-5-9-5.5-19.5T75-180l370-640q6-10 15.5-15t19.5-5q10 0 19.5 5t15.5 15l370 640q6 10 5.5 20.5T885-140q-5 9-14 14.5t-20 5.5H109Zm371-120q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm0-120q17 0 28.5-11.5T520-400v-120q0-17-11.5-28.5T480-560q-17 0-28.5 11.5T440-520v120q0 17 11.5 28.5T480-360Z"/>
</svg>`;
text = 'Possível Spam/Golpe Detectado';
break;
case 'offensive':
icon = `<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#ffdd00">
<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>`;
text = 'Conteúdo Ofensivo Detectado';
break;
}
overlay.innerHTML = `
<div class="warning-icon">${icon}</div>
<div class="warning-text">${text}</div>
`;
element.appendChild(overlay);
}
// Processa notificações push
processPushNotification(notification) {
try {
const text = notification.body || '';
const title = notification.title || '';
// Verifica título e corpo da notificação
const titleCheck = this.isSpam(title);
const bodyCheck = this.isSpam(text);
// Se qualquer parte da notificação for imprópria
if (titleCheck.isSpam || bodyCheck.isSpam) {
const contentType = titleCheck.contentType || bodyCheck.contentType;
const scores = {
title: titleCheck.scores,
body: bodyCheck.scores
};
// Cria uma notificação segura
const safeNotification = {
title: this.getSafeNotificationTitle(contentType),
body: this.getSafeNotificationBody(contentType),
icon: this.getWarningIcon(contentType),
tag: `blocked-${Date.now()}`,
data: {
originalType: contentType,
scores: scores,
timestamp: Date.now()
},
requireInteraction: true,
silent: false,
vibrate: [200, 100, 200],
actions: [
{
action: 'view',
title: 'Ver Detalhes'
},
{
action: 'close',
title: 'Fechar'
}
]
};
// Registra a notificação bloqueada para análise
this.logBlockedNotification({
originalTitle: title,
originalBody: text,
contentType: contentType,
scores: scores,
timestamp: Date.now()
});
return safeNotification;
}
// Se a notificação for segura, adiciona metadados
return {
...notification,
data: {
...notification.data,
safeContent: true,
timestamp: Date.now()
}
};
} catch (error) {
console.error('Erro ao processar notificação:', error);
return null;
}
}
// Obtém título seguro para notificação
getSafeNotificationTitle(contentType) {
switch(contentType) {
case 'explicit':
return '🚫 Conteúdo Explícito Bloqueado';
case 'spam':
return '🚫 Spam Detectado';
case 'offensive':
return '🚫 Conteúdo Ofensivo Bloqueado';
case 'scam':
return '🚫 Possível Golpe Detectado';
default:
return '🚫 Conteúdo Bloqueado';
}
}
// Obtém corpo seguro para notificação
getSafeNotificationBody(contentType) {
switch(contentType) {
case 'explicit':
return 'Uma notificação com conteúdo explícito foi bloqueada para sua segurança.';
case 'spam':
return 'Uma notificação de spam foi bloqueada.';
case 'offensive':
return 'Uma notificação com conteúdo ofensivo foi bloqueada.';
case 'scam':
return 'Uma notificação suspeita foi bloqueada para sua segurança.';
default:
return 'Uma notificação imprópria foi bloqueada.';
}
}
// Obtém ícone de aviso apropriado
getWarningIcon(contentType) {
switch(contentType) {
case 'explicit':
return '/images/warning-explicit.png';
case 'spam':
return '/images/warning-spam.png';
case 'offensive':
return '/images/warning-offensive.png';
case 'scam':
return '/images/warning-scam.png';
default:
return '/images/warning-default.png';
}
}
// Registra notificação bloqueada para análise
logBlockedNotification(data) {
try {
const blockedNotifications = JSON.parse(localStorage.getItem('blockedNotifications') || '[]');
blockedNotifications.push(data);
// Mantém apenas as últimas 100 notificações
if (blockedNotifications.length > 100) {
blockedNotifications.shift();
}
localStorage.setItem('blockedNotifications', JSON.stringify(blockedNotifications));
} catch (error) {
console.error('Erro ao registrar notificação bloqueada:', error);
}
}
// Processa um arquivo antes de enviar
async processFile(file) {
try {
// Verifica o arquivo no servidor proxy
const formData = new FormData();
formData.append('file', file);
const response = await fetch('http://localhost:3001/check-content', {
method: 'POST',
body: formData
});
const result = await response.json();
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;
}
}
}
// Adiciona estilos para o diálogo de aviso
const style = document.createElement('style');
style.textContent = `
.content-moderation-warning {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
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: 16px;
max-width: 500px;
text-align: center;
color: white;
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: 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 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: 28px;
font-weight: bold;
margin-bottom: 10px;
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: 18px;
color: #e3e3e3;
line-height: 1.5;
}
.media-preview {
position: relative;
width: 100%;
max-height: 300px;
border-radius: 12px;
overflow: hidden;
margin-top: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.media-preview img,
.media-preview video {
width: 100%;
height: 100%;
object-fit: cover;
transition: filter 0.3s ease;
}
.blurred img,
.blurred video {
filter: blur(25px) brightness(0.7);
}
.warning-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
width: 100%;
position: absolute;
bottom: 30px;
left: 0;
z-index: 2;
}
.warning-buttons button {
padding: 14px 28px;
border: none;
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: linear-gradient(45deg, #ff4444, #ff0000);
color: white;
box-shadow: 0 4px 15px rgba(255, 0, 0, 0.3);
}
.btn-cancel:hover {
background: linear-gradient(45deg, #ff0000, #cc0000);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 0, 0, 0.4);
}
.btn-view {
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.btn-view:hover {
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);
export default ContentModeration;