diff --git a/public/scripts/content-moderation.js b/public/scripts/content-moderation.js index a590585..bc92517 100644 --- a/public/scripts/content-moderation.js +++ b/public/scripts/content-moderation.js @@ -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 = ` - - `; - 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 = ` + + + + `; + title = 'Conteúdo Explícito'; + message = 'Este conteúdo pode conter material adulto ou impróprio'; + break; + case 'profanity': + icon = ` + + + `; + title = 'Linguagem Imprópria'; + message = 'Este conteúdo contém linguagem ofensiva ou inadequada'; + break; + case 'scam': + icon = ` + + + + `; + title = 'Possível Golpe'; + message = 'Este conteúdo pode ser uma tentativa de golpe ou fraude'; + break; + default: + icon = ` + + + + `; + title = 'Possível Spam'; + message = 'Este conteúdo pode ser spam ou tentativa de golpe'; + } - case 'profanity': - iconSvg = ` - - `; - warningTitle = 'Linguagem Imprópria'; - warningMessage = 'Este conteúdo contém linguagem ofensiva ou inadequada'; - break; - - case 'spam': - iconSvg = ` - - `; - 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 = ` -
- ${iconSvg} -
-
-

${warningTitle}

-

${warningMessage}

-
-
- Preview -
`; - } else if (file.type.startsWith('video/')) { - mediaPreview = ` -
- ${iconSvg} -
-
-

${warningTitle}

-

${warningMessage}

-
-
- -
`; + const content = document.createElement('div'); + content.className = 'warning-content'; + content.innerHTML = ` +
+ ${icon} +
+
+

${title}

+

${message}

+
+
+ ${file.type.startsWith('image/') ? + `Preview` : + `` } - } else { - // Para conteúdo que não é mídia (texto, etc) - mediaPreview = ` -
- ${iconSvg} -
-
-

${warningTitle}

-

${warningMessage}

-
`; +
+
+ + +
+ `; + + 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 = ` -
- ${mediaPreview} -
- - -
-
- `; - - 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);