Update content-moderation.js
This commit is contained in:
parent
acb5e5d98e
commit
af6e52cd99
|
@ -41,6 +41,27 @@ class ContentModeration {
|
||||||
'shut up', 'cala a boca'
|
'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 = [
|
this.spamPatterns = [
|
||||||
/(.)\1{10,}/, // Caracteres repetidos (aumentado para 10+ repetições)
|
/(.)\1{10,}/, // Caracteres repetidos (aumentado para 10+ repetições)
|
||||||
/(.){1000,}/, // Textos muito longos (aumentado para 1000+ caracteres)
|
/(.){1000,}/, // Textos muito longos (aumentado para 1000+ caracteres)
|
||||||
|
@ -94,22 +115,66 @@ class ContentModeration {
|
||||||
'я': 'ya' // cirílico 'я' vs latino 'ya'
|
'я': 'ya' // cirílico 'я' vs latino 'ya'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inicializa o modelo NSFW
|
// Múltiplos modelos NSFW
|
||||||
this.nsfwModel = null;
|
this.nsfwModels = {
|
||||||
this.nsfwModelLoading = false;
|
default: null,
|
||||||
this.loadNSFWModel();
|
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 loadNSFWModel() {
|
async loadAllModels() {
|
||||||
if (this.nsfwModel || this.nsfwModelLoading) return;
|
if (this.modelLoading) return;
|
||||||
this.nsfwModelLoading = true;
|
this.modelLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.nsfwModel = await nsfwjs.load();
|
console.log('Carregando múltiplos modelos NSFW...');
|
||||||
console.log('Modelo NSFW carregado!');
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Erro ao carregar modelo NSFW:', error);
|
console.error('Erro ao carregar modelos:', error);
|
||||||
}
|
}
|
||||||
this.nsfwModelLoading = false;
|
|
||||||
|
this.modelLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normaliza o texto removendo substituições de caracteres
|
// Normaliza o texto removendo substituições de caracteres
|
||||||
|
@ -140,76 +205,289 @@ class ContentModeration {
|
||||||
// Verifica se o conteúdo é NSFW
|
// Verifica se o conteúdo é NSFW
|
||||||
async checkNSFW(file) {
|
async checkNSFW(file) {
|
||||||
if (!this.isMediaFile(file)) return false;
|
if (!this.isMediaFile(file)) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Iniciando verificação NSFW completa para:', file.name);
|
||||||
|
|
||||||
|
// Verifica o nome do arquivo
|
||||||
const fileName = file.name.toLowerCase();
|
const fileName = file.name.toLowerCase();
|
||||||
if (this.blockedWords.some(word => fileName.includes(word.toLowerCase()))) {
|
if (this.blockedWords.some(word => fileName.includes(word.toLowerCase()))) {
|
||||||
|
console.log('Nome do arquivo contém palavras bloqueadas');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (file.type.startsWith('image/')) {
|
|
||||||
return await this.checkImageNSFW(file);
|
// Garante que os modelos estão carregados
|
||||||
} else if (file.type.startsWith('video/')) {
|
if (!this.nsfwModels.default) {
|
||||||
return await this.checkVideoNSFW(file);
|
await this.loadAllModels();
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Erro ao verificar NSFW:', error);
|
console.error('Erro na verificação NSFW:', error);
|
||||||
return false;
|
return {
|
||||||
|
isNSFW: false,
|
||||||
|
confidence: 0,
|
||||||
|
modelResults: {},
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkImageNSFW(file) {
|
async checkImageWithAllModels(file) {
|
||||||
if (!this.nsfwModel) await this.loadNSFWModel();
|
return new Promise(async (resolve) => {
|
||||||
return new Promise((resolve) => {
|
const img = new Image();
|
||||||
const img = new window.Image();
|
img.crossOrigin = 'anonymous';
|
||||||
|
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
const predictions = await this.nsfwModel.classify(img);
|
try {
|
||||||
const isNSFW = predictions.some(p =>
|
console.log('Analisando imagem com múltiplos modelos...');
|
||||||
(p.className === 'Porn' || p.className === 'Hentai' || p.className === 'Sexy') && p.probability > 0.7
|
|
||||||
);
|
// Resultados de cada modelo
|
||||||
resolve(isNSFW);
|
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.4; // Threshold ajustado
|
||||||
|
|
||||||
|
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 = () => resolve(false);
|
|
||||||
|
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);
|
img.src = URL.createObjectURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkVideoNSFW(file) {
|
async checkVideoWithAllModels(file) {
|
||||||
if (!this.nsfwModel) await this.loadNSFWModel();
|
|
||||||
// Extrai frames do vídeo e analisa cada um
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.preload = 'auto';
|
video.preload = 'metadata';
|
||||||
video.src = URL.createObjectURL(file);
|
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.currentTime = 0;
|
|
||||||
video.onloadeddata = async () => {
|
video.onloadedmetadata = async () => {
|
||||||
const canvas = document.createElement('canvas');
|
try {
|
||||||
canvas.width = video.videoWidth;
|
console.log('Analisando vídeo com múltiplos modelos...');
|
||||||
canvas.height = video.videoHeight;
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
let nsfwDetected = false;
|
|
||||||
let framesChecked = 0;
|
// Configurações para análise de frames
|
||||||
const totalFrames = 5;
|
const frameInterval = 1000; // 1 frame por segundo
|
||||||
const duration = video.duration;
|
const maxFrames = Math.min(10, Math.floor(video.duration)); // Máximo 10 frames
|
||||||
for (let i = 1; i <= totalFrames; i++) {
|
let framesAnalyzed = 0;
|
||||||
video.currentTime = (duration * i) / (totalFrames + 1);
|
let totalNSFWScore = 0;
|
||||||
await new Promise(r => video.onseeked = r);
|
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
// Array para armazenar resultados de cada frame
|
||||||
const img = new window.Image();
|
const frameResults = [];
|
||||||
img.src = canvas.toDataURL();
|
|
||||||
await new Promise(r => img.onload = r);
|
// Função para analisar um frame específico
|
||||||
const predictions = await this.nsfwModel.classify(img);
|
const analyzeFrame = async (time) => {
|
||||||
if (predictions.some(p =>
|
video.currentTime = time;
|
||||||
(p.className === 'Porn' || p.className === 'Hentai' || p.className === 'Sexy') && p.probability > 0.7
|
await new Promise(resolve => video.onseeked = resolve);
|
||||||
)) {
|
|
||||||
nsfwDetected = true;
|
canvas.width = video.videoWidth;
|
||||||
break;
|
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++;
|
||||||
}
|
}
|
||||||
framesChecked++;
|
|
||||||
|
// Calcula média final
|
||||||
|
const averageScore = totalNSFWScore / framesAnalyzed;
|
||||||
|
const isNSFW = averageScore > 0.4; // Threshold ajustado
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
resolve(nsfwDetected);
|
|
||||||
};
|
};
|
||||||
video.onerror = () => resolve(false);
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,53 +520,74 @@ class ContentModeration {
|
||||||
|
|
||||||
// Verifica se é spam ou contém palavras bloqueadas
|
// Verifica se é spam ou contém palavras bloqueadas
|
||||||
isSpam(text, file) {
|
isSpam(text, file) {
|
||||||
// Verifica se o texto contém palavras bloqueadas
|
if (!text) return { isSpam: false, contentType: null };
|
||||||
const hasBlockedWords = this.blockedWords.some(word => {
|
|
||||||
|
// 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');
|
const regex = new RegExp(`\\b${word}\\b`, 'i');
|
||||||
return regex.test(text);
|
if (regex.test(normalizedText)) {
|
||||||
});
|
if (this.explicitTerms.includes(word)) explicitScore += 2;
|
||||||
|
else if (this.offensiveTerms.includes(word)) offensiveScore += 2;
|
||||||
// Verifica se o texto contém palavras bloqueadas com substituições
|
else if (this.scamTerms.includes(word)) scamScore += 2;
|
||||||
const hasBlockedWordsWithSubs = this.hasBlockedWordsWithSubstitutions(text);
|
else spamScore += 1;
|
||||||
|
|
||||||
// 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)) {
|
|
||||||
|
// 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';
|
contentType = 'explicit';
|
||||||
}
|
isSpam = true;
|
||||||
if (hasSuspiciousUrls || hasCyrillicChars) {
|
} else if (scamScore >= 3) {
|
||||||
contentType = 'scam';
|
contentType = 'scam';
|
||||||
|
isSpam = true;
|
||||||
|
} else if (offensiveScore >= 2) {
|
||||||
|
contentType = 'offensive';
|
||||||
|
isSpam = true;
|
||||||
|
} else if (spamScore >= 3) {
|
||||||
|
contentType = 'spam';
|
||||||
|
isSpam = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retorna o resultado com o tipo de conteúdo
|
|
||||||
return {
|
return {
|
||||||
isSpam: hasBlockedWords || hasBlockedWordsWithSubs || hasSuspiciousUrls ||
|
isSpam,
|
||||||
hasCyrillicChars || hasInappropriateEmojis || hasSpamPatterns,
|
contentType,
|
||||||
contentType: contentType
|
scores: {
|
||||||
|
explicit: explicitScore,
|
||||||
|
scam: scamScore,
|
||||||
|
offensive: offensiveScore,
|
||||||
|
spam: spamScore
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,53 +732,138 @@ class ContentModeration {
|
||||||
|
|
||||||
// Processa notificações push
|
// Processa notificações push
|
||||||
processPushNotification(notification) {
|
processPushNotification(notification) {
|
||||||
const text = notification.body || '';
|
try {
|
||||||
// Verifica se o texto contém conteúdo ofensivo
|
const text = notification.body || '';
|
||||||
const spamCheck = this.isSpam(text);
|
const title = notification.title || '';
|
||||||
if (spamCheck.isSpam) {
|
|
||||||
try {
|
// Verifica título e corpo da notificação
|
||||||
notification.title = 'Aviso de Moderação';
|
const titleCheck = this.isSpam(title);
|
||||||
notification.icon = '/images/warning-icon.png';
|
const bodyCheck = this.isSpam(text);
|
||||||
// Mensagens específicas para cada tipo de conteúdo
|
|
||||||
switch(spamCheck.contentType) {
|
// Se qualquer parte da notificação for imprópria
|
||||||
case 'explicit':
|
if (titleCheck.isSpam || bodyCheck.isSpam) {
|
||||||
notification.body = '🚫 Conteúdo Explícito Bloqueado';
|
const contentType = titleCheck.contentType || bodyCheck.contentType;
|
||||||
break;
|
const scores = {
|
||||||
case 'spam':
|
title: titleCheck.scores,
|
||||||
notification.body = '🚫 Possível Spam/Golpe Detectado';
|
body: bodyCheck.scores
|
||||||
break;
|
};
|
||||||
case 'offensive':
|
|
||||||
notification.body = '🚫 Conteúdo Ofensivo Detectado';
|
// Cria uma notificação segura
|
||||||
break;
|
const safeNotification = {
|
||||||
case 'scam':
|
title: this.getSafeNotificationTitle(contentType),
|
||||||
notification.body = '🚫 Possível Golpe Detectado';
|
body: this.getSafeNotificationBody(contentType),
|
||||||
break;
|
icon: this.getWarningIcon(contentType),
|
||||||
default:
|
tag: `blocked-${Date.now()}`,
|
||||||
notification.body = '🚫 Conteúdo Bloqueado';
|
data: {
|
||||||
}
|
originalType: contentType,
|
||||||
// Adiciona um timestamp para evitar duplicatas
|
scores: scores,
|
||||||
notification.tag = `blocked-${Date.now()}`;
|
timestamp: Date.now()
|
||||||
notification.options = {
|
},
|
||||||
...notification.options,
|
|
||||||
requireInteraction: true,
|
requireInteraction: true,
|
||||||
|
silent: false,
|
||||||
vibrate: [200, 100, 200],
|
vibrate: [200, 100, 200],
|
||||||
actions: [
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'view',
|
||||||
|
title: 'Ver Detalhes'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'close',
|
action: 'close',
|
||||||
title: 'Fechar'
|
title: 'Fechar'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
// Se por algum motivo não for possível definir o body ou o ícone, retorna null para ocultar a notificação
|
|
||||||
if (!notification.body || !notification.icon) {
|
// Registra a notificação bloqueada para análise
|
||||||
return null;
|
this.logBlockedNotification({
|
||||||
}
|
originalTitle: title,
|
||||||
} catch (e) {
|
originalBody: text,
|
||||||
// Em caso de erro, oculta a notificação
|
contentType: contentType,
|
||||||
return null;
|
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);
|
||||||
}
|
}
|
||||||
return notification;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processa um arquivo antes de enviar
|
// Processa um arquivo antes de enviar
|
||||||
|
|
Loading…
Reference in New Issue