Remove duplicate root-level JavaScript files
- Remove legacy JS files: models/, routes/, services/, server.js, profanity-filter.js - Keep TypeScript source files in src/ as single source of truth - Clean up project structure to eliminate confusion - All functionality now properly contained in src/ directory 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
dc6754a0fc
commit
916deea68a
9 changed files with 0 additions and 1801 deletions
|
@ -1,131 +0,0 @@
|
||||||
class Location {
|
|
||||||
constructor(db) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActive(hoursThreshold = 48) {
|
|
||||||
const cutoffTime = new Date(Date.now() - hoursThreshold * 60 * 60 * 1000).toISOString();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.all(
|
|
||||||
'SELECT * FROM locations WHERE created_at > ? OR persistent = 1 ORDER BY created_at DESC',
|
|
||||||
[cutoffTime],
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.all(
|
|
||||||
'SELECT id, address, description, latitude, longitude, persistent, created_at FROM locations ORDER BY created_at DESC',
|
|
||||||
[],
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(location) {
|
|
||||||
const { address, latitude, longitude, description } = location;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'INSERT INTO locations (address, latitude, longitude, description) VALUES (?, ?, ?, ?)',
|
|
||||||
[address, latitude, longitude, description],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ id: this.lastID, ...location });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(id, location) {
|
|
||||||
const { address, latitude, longitude, description } = location;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'UPDATE locations SET address = ?, latitude = ?, longitude = ?, description = ? WHERE id = ?',
|
|
||||||
[address, latitude, longitude, description, id],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async togglePersistent(id, persistent) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'UPDATE locations SET persistent = ? WHERE id = ?',
|
|
||||||
[persistent ? 1 : 0, id],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'DELETE FROM locations WHERE id = ?',
|
|
||||||
[id],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanupExpired(hoursThreshold = 48) {
|
|
||||||
const cutoffTime = new Date(Date.now() - hoursThreshold * 60 * 60 * 1000).toISOString();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'DELETE FROM locations WHERE created_at < ? AND persistent = 0',
|
|
||||||
[cutoffTime],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeTable() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.serialize(() => {
|
|
||||||
this.db.run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS locations (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
latitude REAL,
|
|
||||||
longitude REAL,
|
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
description TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
`, (err) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
|
|
||||||
this.db.run(`
|
|
||||||
ALTER TABLE locations ADD COLUMN persistent INTEGER DEFAULT 0
|
|
||||||
`, (err) => {
|
|
||||||
if (err && !err.message.includes('duplicate column name')) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Location;
|
|
|
@ -1,96 +0,0 @@
|
||||||
class ProfanityWord {
|
|
||||||
constructor(db) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.all(
|
|
||||||
'SELECT id, word, severity, category, created_at, created_by FROM profanity_words ORDER BY created_at DESC',
|
|
||||||
[],
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadWords() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.all(
|
|
||||||
'SELECT word, severity, category FROM profanity_words',
|
|
||||||
[],
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(word, severity, category, createdBy = 'admin') {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'INSERT INTO profanity_words (word, severity, category, created_by) VALUES (?, ?, ?, ?)',
|
|
||||||
[word.toLowerCase(), severity, category, createdBy],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({
|
|
||||||
id: this.lastID,
|
|
||||||
word: word.toLowerCase(),
|
|
||||||
severity,
|
|
||||||
category,
|
|
||||||
created_by: createdBy
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(id, word, severity, category) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'UPDATE profanity_words SET word = ?, severity = ?, category = ? WHERE id = ?',
|
|
||||||
[word.toLowerCase(), severity, category, id],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'DELETE FROM profanity_words WHERE id = ?',
|
|
||||||
[id],
|
|
||||||
function(err) {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve({ changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeTable() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(`
|
|
||||||
CREATE TABLE IF NOT EXISTS profanity_words (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
word TEXT NOT NULL UNIQUE,
|
|
||||||
severity TEXT NOT NULL,
|
|
||||||
category TEXT NOT NULL,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by TEXT DEFAULT 'system'
|
|
||||||
)
|
|
||||||
`, (err) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ProfanityWord;
|
|
|
@ -1,573 +0,0 @@
|
||||||
/**
|
|
||||||
* Comprehensive Server-Side Profanity Filter for Ice Watch
|
|
||||||
* Filters inappropriate language with database-backed custom word management
|
|
||||||
*/
|
|
||||||
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
class ProfanityFilter {
|
|
||||||
/**
|
|
||||||
* Static factory method for creating and initializing a ProfanityFilter
|
|
||||||
* @param {string|null} dbPath - Optional path to database file
|
|
||||||
* @returns {Promise<ProfanityFilter>} Fully initialized ProfanityFilter instance
|
|
||||||
*/
|
|
||||||
static async create(dbPath = null) {
|
|
||||||
const filter = new ProfanityFilter(dbPath);
|
|
||||||
await filter.initialize();
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(dbPath = null) {
|
|
||||||
// Initialize separate database for profanity filter
|
|
||||||
const defaultDbPath = path.join(__dirname, 'profanity.db');
|
|
||||||
this.dbPath = dbPath || defaultDbPath;
|
|
||||||
this.db = new sqlite3.Database(this.dbPath);
|
|
||||||
this.isInitialized = false;
|
|
||||||
|
|
||||||
// Base profanity words - comprehensive list
|
|
||||||
this.baseProfanityWords = [
|
|
||||||
// Common profanity
|
|
||||||
'damn', 'hell', 'crap', 'shit', 'fuck', 'ass', 'bitch', 'bastard',
|
|
||||||
'piss', 'whore', 'slut', 'retard', 'fag', 'gay', 'homo', 'tranny',
|
|
||||||
'dickhead', 'asshole', 'motherfucker', 'cocksucker', 'twat', 'cunt',
|
|
||||||
|
|
||||||
// Racial slurs and hate speech
|
|
||||||
'nigger', 'nigga', 'spic', 'wetback', 'chink', 'gook', 'kike',
|
|
||||||
'raghead', 'towelhead', 'beaner', 'cracker', 'honkey', 'whitey',
|
|
||||||
'kyke', 'jigaboo', 'coon', 'darkie', 'mammy', 'pickaninny',
|
|
||||||
|
|
||||||
// Sexual content
|
|
||||||
'penis', 'vagina', 'boob', 'tit', 'cock', 'dick', 'pussy', 'cum',
|
|
||||||
'sex', 'porn', 'nude', 'naked', 'horny', 'masturbate', 'orgasm',
|
|
||||||
'blowjob', 'handjob', 'anal', 'penetration', 'erection', 'climax',
|
|
||||||
|
|
||||||
// Violence and threats
|
|
||||||
'kill', 'murder', 'shoot', 'bomb', 'terrorist', 'suicide', 'rape',
|
|
||||||
'violence', 'assault', 'attack', 'threat', 'harm', 'hurt', 'pain',
|
|
||||||
'stab', 'strangle', 'torture', 'execute', 'assassinate', 'slaughter',
|
|
||||||
|
|
||||||
// Drugs and substances
|
|
||||||
'weed', 'marijuana', 'cocaine', 'heroin', 'meth', 'drugs', 'high',
|
|
||||||
'stoned', 'drunk', 'alcohol', 'beer', 'liquor', 'vodka', 'whiskey',
|
|
||||||
'ecstasy', 'lsd', 'crack', 'dope', 'pot', 'joint', 'bong',
|
|
||||||
|
|
||||||
// Religious/cultural insults
|
|
||||||
'jesus christ', 'goddamn', 'christ almighty', 'holy shit', 'god damn',
|
|
||||||
'for christ sake', 'jesus fucking christ', 'holy fuck',
|
|
||||||
|
|
||||||
// Body parts (inappropriate context)
|
|
||||||
'testicles', 'balls', 'scrotum', 'clitoris', 'labia', 'anus',
|
|
||||||
'rectum', 'butthole', 'nipples', 'breasts',
|
|
||||||
|
|
||||||
// Misc inappropriate
|
|
||||||
'wtf', 'omfg', 'stfu', 'gtfo', 'milf', 'dilf', 'thot', 'simp',
|
|
||||||
'incel', 'chad', 'beta', 'alpha male', 'mansplain', 'karen'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Leetspeak and common substitutions
|
|
||||||
this.leetMap = {
|
|
||||||
'0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '6': 'g', '7': 't',
|
|
||||||
'8': 'b', '9': 'g', '@': 'a', '$': 's', '!': 'i', '+': 't', '*': 'a',
|
|
||||||
'%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize custom words array
|
|
||||||
this.customWords = [];
|
|
||||||
|
|
||||||
// Initialize patterns to null; will be built during async initialization
|
|
||||||
this.patterns = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async initialization method that must be called after construction
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async initialize() {
|
|
||||||
if (this.isInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize database synchronously first
|
|
||||||
await this.initializeDatabaseAsync();
|
|
||||||
|
|
||||||
// Load custom words from database
|
|
||||||
await this.loadCustomWords();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
console.log('ProfanityFilter initialization completed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during ProfanityFilter initialization:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the database table for custom profanity words (async version)
|
|
||||||
*/
|
|
||||||
async initializeDatabaseAsync() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.serialize(() => {
|
|
||||||
this.db.run(`CREATE TABLE IF NOT EXISTS profanity_words (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
word TEXT NOT NULL UNIQUE,
|
|
||||||
severity TEXT DEFAULT 'medium',
|
|
||||||
category TEXT DEFAULT 'custom',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_by TEXT DEFAULT 'admin'
|
|
||||||
)`, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error creating profanity_words table:', err);
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
console.log('Profanity words table initialized successfully');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed the deprecated initializeDatabase method as it is unused and no longer necessary.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load custom words from database
|
|
||||||
*/
|
|
||||||
async loadCustomWords() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.all(
|
|
||||||
'SELECT word, severity, category FROM profanity_words',
|
|
||||||
[],
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error loading custom profanity words:', err);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.customWords = rows.map(row => ({
|
|
||||||
word: row.word.toLowerCase(),
|
|
||||||
severity: row.severity,
|
|
||||||
category: row.category
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(`Loaded ${this.customWords.length} custom profanity words`);
|
|
||||||
this.patterns = this.buildPatterns(); // Rebuild patterns with custom words
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the filter is fully initialized and warn if not
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_checkInitialization() {
|
|
||||||
if (!this.isInitialized) {
|
|
||||||
console.warn('⚠️ ProfanityFilter: Using base words only - custom words not loaded yet. Call initialize() or use ProfanityFilter.create() for full functionality.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all profanity words (base + custom)
|
|
||||||
*/
|
|
||||||
getAllWords() {
|
|
||||||
this._checkInitialization();
|
|
||||||
|
|
||||||
const baseWords = this.baseProfanityWords.map(word => ({
|
|
||||||
word: word.toLowerCase(),
|
|
||||||
severity: this.getSeverity(word),
|
|
||||||
category: 'base'
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [...baseWords, ...this.customWords];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build regex patterns for profanity detection
|
|
||||||
*/
|
|
||||||
buildPatterns() {
|
|
||||||
const allWords = this.getAllWords();
|
|
||||||
|
|
||||||
return allWords.map(wordObj => {
|
|
||||||
const word = wordObj.word;
|
|
||||||
// Create simple word boundary pattern first
|
|
||||||
let pattern = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape special chars
|
|
||||||
|
|
||||||
// For simple detection, use word boundaries
|
|
||||||
const simpleRegex = new RegExp(`\\b${pattern}\\b`, 'gi');
|
|
||||||
|
|
||||||
// Also create pattern with character substitutions for advanced detection
|
|
||||||
const advancedPattern = word.split('').map(char => {
|
|
||||||
const substitutes = this.getCharSubstitutes(char);
|
|
||||||
if (substitutes.length > 1) {
|
|
||||||
// Escape special regex characters in substitutes
|
|
||||||
const escapedSubs = substitutes.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
||||||
return `[${escapedSubs.join('')}]`;
|
|
||||||
}
|
|
||||||
return char;
|
|
||||||
}).join('[\\s\\-_\\.,;:!?]*?');
|
|
||||||
|
|
||||||
const advancedRegex = new RegExp(`\\b${advancedPattern}\\b`, 'gi');
|
|
||||||
|
|
||||||
return {
|
|
||||||
regex: simpleRegex,
|
|
||||||
advancedRegex: advancedRegex,
|
|
||||||
word: word,
|
|
||||||
severity: wordObj.severity,
|
|
||||||
category: wordObj.category
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get character substitutes including leetspeak
|
|
||||||
*/
|
|
||||||
getCharSubstitutes(char) {
|
|
||||||
const substitutes = [char];
|
|
||||||
|
|
||||||
// Add leetspeak equivalents
|
|
||||||
Object.keys(this.leetMap).forEach(leet => {
|
|
||||||
if (this.leetMap[leet] === char) {
|
|
||||||
substitutes.push(leet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add common character substitutions
|
|
||||||
const charMap = {
|
|
||||||
'a': ['@', '4', '*'],
|
|
||||||
'e': ['3'],
|
|
||||||
'i': ['1', '!', '|'],
|
|
||||||
'o': ['0'],
|
|
||||||
's': ['$', '5'],
|
|
||||||
't': ['7', '+'],
|
|
||||||
'g': ['6', '9'],
|
|
||||||
'b': ['8'],
|
|
||||||
'l': ['1', '|']
|
|
||||||
};
|
|
||||||
|
|
||||||
if (charMap[char]) {
|
|
||||||
substitutes.push(...charMap[char]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...new Set(substitutes)]; // Remove duplicates
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize text to handle various obfuscation attempts
|
|
||||||
*/
|
|
||||||
normalizeText(text) {
|
|
||||||
if (!text || typeof text !== 'string') return '';
|
|
||||||
|
|
||||||
let normalized = text.toLowerCase();
|
|
||||||
|
|
||||||
// Replace leetspeak characters - escape special regex characters properly
|
|
||||||
Object.keys(this.leetMap).forEach(leet => {
|
|
||||||
const escapedLeet = leet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
normalized = normalized.replace(new RegExp(escapedLeet, 'g'), this.leetMap[leet]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle spaced out words (f u c k -> fuck) - more comprehensive
|
|
||||||
normalized = normalized.replace(/\b([a-z])\s+([a-z])\s+([a-z])\s+([a-z])\s+([a-z])\b/g, '$1$2$3$4$5');
|
|
||||||
normalized = normalized.replace(/\b([a-z])\s+([a-z])\s+([a-z])\s+([a-z])\b/g, '$1$2$3$4');
|
|
||||||
normalized = normalized.replace(/\b([a-z])\s+([a-z])\s+([a-z])\b/g, '$1$2$3');
|
|
||||||
|
|
||||||
// Remove excessive punctuation but keep word boundaries
|
|
||||||
normalized = normalized.replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if text contains profanity
|
|
||||||
*/
|
|
||||||
containsProfanity(text) {
|
|
||||||
if (!text || typeof text !== 'string') return false;
|
|
||||||
|
|
||||||
this._checkInitialization();
|
|
||||||
|
|
||||||
const normalizedText = this.normalizeText(text);
|
|
||||||
const originalText = text.toLowerCase();
|
|
||||||
|
|
||||||
return this.patterns.some(pattern => {
|
|
||||||
return pattern.regex.test(normalizedText) ||
|
|
||||||
pattern.regex.test(originalText) ||
|
|
||||||
(pattern.advancedRegex && pattern.advancedRegex.test(normalizedText));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get detailed analysis of profanity in text
|
|
||||||
*/
|
|
||||||
analyzeProfanity(text) {
|
|
||||||
if (!text || typeof text !== 'string') {
|
|
||||||
return {
|
|
||||||
hasProfanity: false,
|
|
||||||
matches: [],
|
|
||||||
severity: 'none',
|
|
||||||
count: 0,
|
|
||||||
filtered: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this._checkInitialization();
|
|
||||||
|
|
||||||
const normalizedText = this.normalizeText(text);
|
|
||||||
const originalText = text.toLowerCase();
|
|
||||||
const matches = [];
|
|
||||||
let filteredText = text;
|
|
||||||
|
|
||||||
this.patterns.forEach(pattern => {
|
|
||||||
let found = null;
|
|
||||||
let matchedText = '';
|
|
||||||
|
|
||||||
// Try simple regex first on both normalized and original text
|
|
||||||
found = normalizedText.match(pattern.regex);
|
|
||||||
if (found) {
|
|
||||||
matchedText = normalizedText;
|
|
||||||
} else {
|
|
||||||
found = originalText.match(pattern.regex);
|
|
||||||
if (found) {
|
|
||||||
matchedText = originalText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found, try advanced regex
|
|
||||||
if (!found && pattern.advancedRegex) {
|
|
||||||
found = normalizedText.match(pattern.advancedRegex);
|
|
||||||
if (found) {
|
|
||||||
matchedText = normalizedText;
|
|
||||||
} else {
|
|
||||||
found = originalText.match(pattern.advancedRegex);
|
|
||||||
if (found) {
|
|
||||||
matchedText = originalText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
// Only add if not already detected
|
|
||||||
if (!matches.some(m => m.word === pattern.word)) {
|
|
||||||
matches.push({
|
|
||||||
word: pattern.word,
|
|
||||||
matches: found,
|
|
||||||
severity: pattern.severity,
|
|
||||||
category: pattern.category
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace in original text - try multiple patterns
|
|
||||||
const wordPattern = pattern.word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const simplePattern = new RegExp(`\\b${wordPattern}\\b`, 'gi');
|
|
||||||
filteredText = filteredText.replace(simplePattern, '*'.repeat(Math.max(3, pattern.word.length)));
|
|
||||||
|
|
||||||
// Also try to replace with the exact matches found
|
|
||||||
found.forEach(match => {
|
|
||||||
const exactPattern = new RegExp(match.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
||||||
filteredText = filteredText.replace(exactPattern, '*'.repeat(Math.max(3, match.length)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also try to replace with advanced pattern
|
|
||||||
if (pattern.advancedRegex) {
|
|
||||||
filteredText = filteredText.replace(pattern.advancedRegex, '*'.repeat(Math.max(3, pattern.word.length)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasProfanity = matches.length > 0;
|
|
||||||
const maxSeverity = hasProfanity ?
|
|
||||||
Math.max(...matches.map(m => this.getSeverityLevel(m.severity))) : 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasProfanity,
|
|
||||||
matches,
|
|
||||||
severity: this.getSeverityName(maxSeverity),
|
|
||||||
count: matches.length,
|
|
||||||
filtered: filteredText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter profanity from text
|
|
||||||
*/
|
|
||||||
filterProfanity(text, replacement = '*') {
|
|
||||||
const analysis = this.analyzeProfanity(text);
|
|
||||||
let filteredText = text;
|
|
||||||
for (const match of analysis.matches) {
|
|
||||||
const regex = new RegExp(`\\b${match.word}\\b`, 'gi');
|
|
||||||
filteredText = filteredText.replace(regex, replacement.repeat(match.word.length));
|
|
||||||
}
|
|
||||||
return filteredText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get severity level for a word
|
|
||||||
*/
|
|
||||||
getSeverity(word) {
|
|
||||||
// High severity: hate speech, extreme profanity, threats
|
|
||||||
const highSeverity = [
|
|
||||||
'nigger', 'nigga', 'kill', 'murder', 'shoot', 'bomb', 'terrorist',
|
|
||||||
'rape', 'kike', 'fag', 'raghead', 'towelhead', 'motherfucker',
|
|
||||||
'cunt', 'cocksucker', 'jigaboo', 'coon', 'execute', 'assassinate'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Medium severity: sexual content, moderate profanity
|
|
||||||
const mediumSeverity = [
|
|
||||||
'fuck', 'shit', 'bitch', 'whore', 'slut', 'penis', 'vagina',
|
|
||||||
'cock', 'dick', 'pussy', 'cum', 'sex', 'porn', 'asshole',
|
|
||||||
'dickhead', 'twat', 'blowjob', 'handjob'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (highSeverity.includes(word.toLowerCase())) return 'high';
|
|
||||||
if (mediumSeverity.includes(word.toLowerCase())) return 'medium';
|
|
||||||
return 'low';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get numeric severity level
|
|
||||||
*/
|
|
||||||
getSeverityLevel(severity) {
|
|
||||||
switch (severity) {
|
|
||||||
case 'high': return 3;
|
|
||||||
case 'medium': return 2;
|
|
||||||
case 'low': return 1;
|
|
||||||
default: return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get severity name from level
|
|
||||||
*/
|
|
||||||
getSeverityName(level) {
|
|
||||||
switch (level) {
|
|
||||||
case 3: return 'high';
|
|
||||||
case 2: return 'medium';
|
|
||||||
case 1: return 'low';
|
|
||||||
default: return 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a custom word to the database
|
|
||||||
*/
|
|
||||||
async addCustomWord(word, severity = 'medium', category = 'custom', createdBy = 'admin') {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const normalizedWord = word.toLowerCase().trim();
|
|
||||||
|
|
||||||
this.db.run(
|
|
||||||
'INSERT INTO profanity_words (word, severity, category, created_by) VALUES (?, ?, ?, ?)',
|
|
||||||
[normalizedWord, severity, category, createdBy],
|
|
||||||
function(err) {
|
|
||||||
if (err) {
|
|
||||||
if (err.message.includes('UNIQUE constraint failed')) {
|
|
||||||
reject(new Error('Word already exists in the filter'));
|
|
||||||
} else {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Added custom profanity word: ${normalizedWord}`);
|
|
||||||
resolve({
|
|
||||||
id: this.lastID,
|
|
||||||
word: normalizedWord,
|
|
||||||
severity,
|
|
||||||
category,
|
|
||||||
created_by: createdBy
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a custom word from the database
|
|
||||||
*/
|
|
||||||
async removeCustomWord(wordId) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.run(
|
|
||||||
'DELETE FROM profanity_words WHERE id = ?',
|
|
||||||
[wordId],
|
|
||||||
function(err) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.changes === 0) {
|
|
||||||
reject(new Error('Word not found'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Removed custom profanity word with ID: ${wordId}`);
|
|
||||||
resolve({ deleted: true, changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all custom words from database
|
|
||||||
*/
|
|
||||||
async getCustomWords() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.db.all(
|
|
||||||
'SELECT id, word, severity, category, created_at, created_by FROM profanity_words ORDER BY created_at DESC',
|
|
||||||
[],
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a custom word
|
|
||||||
*/
|
|
||||||
async updateCustomWord(wordId, updates) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const { word, severity, category } = updates;
|
|
||||||
|
|
||||||
this.db.run(
|
|
||||||
'UPDATE profanity_words SET word = ?, severity = ?, category = ? WHERE id = ?',
|
|
||||||
[word.toLowerCase().trim(), severity, category, wordId],
|
|
||||||
function(err) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.changes === 0) {
|
|
||||||
reject(new Error('Word not found'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Updated custom profanity word with ID: ${wordId}`);
|
|
||||||
resolve({ updated: true, changes: this.changes });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the database connection
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
if (this.db) {
|
|
||||||
this.db.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error closing profanity filter database:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ProfanityFilter;
|
|
231
routes/admin.js
231
routes/admin.js
|
@ -1,231 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
module.exports = (locationModel, profanityWordModel, profanityFilter, authenticateAdmin) => {
|
|
||||||
// Admin login
|
|
||||||
router.post('/login', (req, res) => {
|
|
||||||
console.log('Admin login attempt');
|
|
||||||
const { password } = req.body;
|
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
|
|
||||||
|
|
||||||
if (password === ADMIN_PASSWORD) {
|
|
||||||
console.log('Admin login successful');
|
|
||||||
res.json({ token: ADMIN_PASSWORD, message: 'Login successful' });
|
|
||||||
} else {
|
|
||||||
console.warn('Admin login failed: invalid password');
|
|
||||||
res.status(401).json({ error: 'Invalid password' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all locations for admin (including expired ones)
|
|
||||||
router.get('/locations', authenticateAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const rows = await locationModel.getAll();
|
|
||||||
|
|
||||||
// Process and clean data before sending
|
|
||||||
const locations = rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
address: row.address,
|
|
||||||
description: row.description || '',
|
|
||||||
latitude: row.latitude,
|
|
||||||
longitude: row.longitude,
|
|
||||||
persistent: !!row.persistent,
|
|
||||||
created_at: row.created_at,
|
|
||||||
isActive: new Date(row.created_at) > new Date(Date.now() - 48 * 60 * 60 * 1000)
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json(locations);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching all locations:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update a location (admin only)
|
|
||||||
router.put('/locations/:id', authenticateAdmin, async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { address, latitude, longitude, description } = req.body;
|
|
||||||
|
|
||||||
if (!address) {
|
|
||||||
res.status(400).json({ error: 'Address is required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await locationModel.update(id, {
|
|
||||||
address,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
description
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.changes === 0) {
|
|
||||||
res.status(404).json({ error: 'Location not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ message: 'Location updated successfully' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error updating location:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toggle persistent status of a location (admin only)
|
|
||||||
router.patch('/locations/:id/persistent', authenticateAdmin, async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { persistent } = req.body;
|
|
||||||
|
|
||||||
if (typeof persistent !== 'boolean') {
|
|
||||||
res.status(400).json({ error: 'Persistent value must be a boolean' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await locationModel.togglePersistent(id, persistent);
|
|
||||||
|
|
||||||
if (result.changes === 0) {
|
|
||||||
res.status(404).json({ error: 'Location not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Location ${id} persistent status set to ${persistent}`);
|
|
||||||
res.json({ message: 'Persistent status updated successfully', persistent });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error updating persistent status:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete a location (admin authentication required)
|
|
||||||
router.delete('/locations/:id', authenticateAdmin, async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await locationModel.delete(id);
|
|
||||||
|
|
||||||
if (result.changes === 0) {
|
|
||||||
res.status(404).json({ error: 'Location not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ message: 'Location deleted successfully' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error deleting location:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Profanity Management Routes
|
|
||||||
|
|
||||||
// Get all custom profanity words (admin only)
|
|
||||||
router.get('/profanity-words', authenticateAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const words = await profanityFilter.getCustomWords();
|
|
||||||
res.json(words);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching custom profanity words:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a custom profanity word (admin only)
|
|
||||||
router.post('/profanity-words', authenticateAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { word, severity = 'medium', category = 'custom' } = req.body;
|
|
||||||
|
|
||||||
if (!word || typeof word !== 'string' || word.trim().length === 0) {
|
|
||||||
return res.status(400).json({ error: 'Word is required and must be a non-empty string' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['low', 'medium', 'high'].includes(severity)) {
|
|
||||||
return res.status(400).json({ error: 'Severity must be low, medium, or high' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await profanityFilter.addCustomWord(word, severity, category, 'admin');
|
|
||||||
await profanityFilter.loadCustomWords(); // Reload to update patterns
|
|
||||||
|
|
||||||
console.log(`Admin added custom profanity word: ${word}`);
|
|
||||||
res.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding custom profanity word:', error);
|
|
||||||
if (error.message.includes('already exists')) {
|
|
||||||
res.status(409).json({ error: error.message });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update a custom profanity word (admin only)
|
|
||||||
router.put('/profanity-words/:id', authenticateAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { word, severity, category } = req.body;
|
|
||||||
|
|
||||||
if (!word || typeof word !== 'string' || word.trim().length === 0) {
|
|
||||||
return res.status(400).json({ error: 'Word is required and must be a non-empty string' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['low', 'medium', 'high'].includes(severity)) {
|
|
||||||
return res.status(400).json({ error: 'Severity must be low, medium, or high' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await profanityFilter.updateCustomWord(id, { word, severity, category });
|
|
||||||
await profanityFilter.loadCustomWords(); // Reload to update patterns
|
|
||||||
|
|
||||||
console.log(`Admin updated custom profanity word ID ${id}`);
|
|
||||||
res.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating custom profanity word:', error);
|
|
||||||
if (error.message.includes('not found')) {
|
|
||||||
res.status(404).json({ error: error.message });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete a custom profanity word (admin only)
|
|
||||||
router.delete('/profanity-words/:id', authenticateAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
const result = await profanityFilter.removeCustomWord(id);
|
|
||||||
await profanityFilter.loadCustomWords(); // Reload to update patterns
|
|
||||||
|
|
||||||
console.log(`Admin deleted custom profanity word ID ${id}`);
|
|
||||||
res.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting custom profanity word:', error);
|
|
||||||
if (error.message.includes('not found')) {
|
|
||||||
res.status(404).json({ error: error.message });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test profanity filter (admin only) - for testing purposes
|
|
||||||
router.post('/test-profanity', authenticateAdmin, (req, res) => {
|
|
||||||
try {
|
|
||||||
const { text } = req.body;
|
|
||||||
|
|
||||||
if (!text || typeof text !== 'string') {
|
|
||||||
return res.status(400).json({ error: 'Text is required for testing' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysis = profanityFilter.analyzeProfanity(text);
|
|
||||||
res.json({
|
|
||||||
original: text,
|
|
||||||
analysis: analysis,
|
|
||||||
filtered: analysis.filtered
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error testing profanity filter:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
// Get API configuration
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
console.log('📡 API Config requested');
|
|
||||||
const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN || null;
|
|
||||||
|
|
||||||
console.log('MapBox token present:', !!MAPBOX_ACCESS_TOKEN);
|
|
||||||
console.log('MapBox token starts with pk:', MAPBOX_ACCESS_TOKEN?.startsWith('pk.'));
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
// MapBox tokens are designed to be public (they have domain restrictions)
|
|
||||||
mapboxAccessToken: MAPBOX_ACCESS_TOKEN,
|
|
||||||
hasMapbox: !!MAPBOX_ACCESS_TOKEN
|
|
||||||
// SECURITY: Google Maps API key is kept server-side only
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
};
|
|
|
@ -1,97 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
module.exports = (locationModel, profanityFilter) => {
|
|
||||||
// Get all active locations (within 48 hours OR persistent)
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
console.log('Fetching active locations');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const locations = await locationModel.getActive();
|
|
||||||
console.log(`Fetched ${locations.length} active locations (including persistent)`);
|
|
||||||
res.json(locations);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching locations:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a new location
|
|
||||||
router.post('/', async (req, res) => {
|
|
||||||
const { address, latitude, longitude } = req.body;
|
|
||||||
let { description } = req.body;
|
|
||||||
console.log(`Attempt to add new location: ${address}`);
|
|
||||||
|
|
||||||
if (!address) {
|
|
||||||
console.warn('Failed to add location: Address is required');
|
|
||||||
res.status(400).json({ error: 'Address is required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for profanity in description and reject if any is found
|
|
||||||
if (description && profanityFilter) {
|
|
||||||
try {
|
|
||||||
const analysis = profanityFilter.analyzeProfanity(description);
|
|
||||||
if (analysis.hasProfanity) {
|
|
||||||
console.warn(`Submission rejected due to inappropriate language (${analysis.count} word${analysis.count > 1 ? 's' : ''}, severity: ${analysis.severity}) - Original: "${req.body.description}"`);
|
|
||||||
|
|
||||||
// Reject any submission with profanity
|
|
||||||
const wordText = analysis.count === 1 ? 'word' : 'words';
|
|
||||||
const detectedWords = analysis.matches.map(m => m.word).join(', ');
|
|
||||||
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Submission rejected',
|
|
||||||
message: `Your description contains inappropriate language and cannot be posted. Please revise your description to focus on road conditions and keep it professional.\n\nExample: "Multiple vehicles stuck, black ice present" or "Road very slippery, saw 3 accidents"`,
|
|
||||||
details: {
|
|
||||||
severity: analysis.severity,
|
|
||||||
wordCount: analysis.count,
|
|
||||||
detectedCategories: [...new Set(analysis.matches.map(m => m.category))]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (filterError) {
|
|
||||||
console.error('Error checking profanity:', filterError);
|
|
||||||
// Continue with original description if filter fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newLocation = await locationModel.create({
|
|
||||||
address,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
description
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Location added successfully: ${address}`);
|
|
||||||
res.json({
|
|
||||||
...newLocation,
|
|
||||||
created_at: new Date().toISOString()
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error inserting location:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Legacy delete route (keeping for backwards compatibility)
|
|
||||||
router.delete('/:id', async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await locationModel.delete(id);
|
|
||||||
|
|
||||||
if (result.changes === 0) {
|
|
||||||
res.status(404).json({ error: 'Location not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ message: 'Location deleted successfully' });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error deleting location:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
};
|
|
223
server.js
223
server.js
|
@ -1,223 +0,0 @@
|
||||||
require('dotenv').config({ path: '.env.local' });
|
|
||||||
require('dotenv').config();
|
|
||||||
const express = require('express');
|
|
||||||
const cors = require('cors');
|
|
||||||
const path = require('path');
|
|
||||||
const cron = require('node-cron');
|
|
||||||
const DatabaseService = require('./services/DatabaseService');
|
|
||||||
const ProfanityFilterService = require('./services/ProfanityFilterService');
|
|
||||||
|
|
||||||
// Import route modules
|
|
||||||
const configRoutes = require('./routes/config');
|
|
||||||
const locationRoutes = require('./routes/locations');
|
|
||||||
const adminRoutes = require('./routes/admin');
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
// Database and services setup
|
|
||||||
const databaseService = new DatabaseService();
|
|
||||||
let profanityFilter;
|
|
||||||
|
|
||||||
// Create fallback filter function
|
|
||||||
function createFallbackFilter() {
|
|
||||||
return {
|
|
||||||
// Core profanity checking methods
|
|
||||||
containsProfanity: () => false,
|
|
||||||
analyzeProfanity: (text) => ({
|
|
||||||
hasProfanity: false,
|
|
||||||
matches: [],
|
|
||||||
severity: 'none',
|
|
||||||
count: 0,
|
|
||||||
filtered: text || ''
|
|
||||||
}),
|
|
||||||
filterProfanity: (text) => text || '',
|
|
||||||
|
|
||||||
// Database management methods used by admin routes
|
|
||||||
addCustomWord: (word, severity, category, createdBy) => Promise.resolve({
|
|
||||||
id: null,
|
|
||||||
word: word || null,
|
|
||||||
severity: severity || null,
|
|
||||||
category: category || null,
|
|
||||||
createdBy: createdBy || null,
|
|
||||||
success: false,
|
|
||||||
error: 'Profanity filter not available - please check server configuration'
|
|
||||||
}),
|
|
||||||
removeCustomWord: (wordId) => Promise.resolve({
|
|
||||||
success: false,
|
|
||||||
error: 'Profanity filter not available - please check server configuration'
|
|
||||||
}),
|
|
||||||
updateCustomWord: (wordId, updates) => Promise.resolve({
|
|
||||||
success: false,
|
|
||||||
error: 'Profanity filter not available - please check server configuration'
|
|
||||||
}),
|
|
||||||
getCustomWords: () => Promise.resolve([]),
|
|
||||||
loadCustomWords: () => Promise.resolve(),
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
getAllWords: () => [],
|
|
||||||
getSeverity: () => 'none',
|
|
||||||
getSeverityLevel: () => 0,
|
|
||||||
getSeverityName: () => 'none',
|
|
||||||
normalizeText: (text) => text || '',
|
|
||||||
buildPatterns: () => [],
|
|
||||||
|
|
||||||
// Cleanup method
|
|
||||||
close: () => {},
|
|
||||||
|
|
||||||
// Special property to identify this as a fallback filter
|
|
||||||
_isFallback: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize profanity filter asynchronously
|
|
||||||
async function initializeProfanityFilter() {
|
|
||||||
try {
|
|
||||||
const profanityWordModel = databaseService.getProfanityWordModel();
|
|
||||||
profanityFilter = new ProfanityFilterService(profanityWordModel);
|
|
||||||
await profanityFilter.initialize();
|
|
||||||
console.log('Profanity filter initialized successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('WARNING: Failed to initialize profanity filter:', error);
|
|
||||||
console.error('Creating fallback no-op profanity filter. ALL CONTENT WILL BE ALLOWED!');
|
|
||||||
console.error('This is a security risk - please fix the profanity filter configuration.');
|
|
||||||
|
|
||||||
profanityFilter = createFallbackFilter();
|
|
||||||
console.warn('⚠️ SECURITY WARNING: Profanity filtering is DISABLED due to initialization failure!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database initialization is now handled by DatabaseService
|
|
||||||
|
|
||||||
// Clean up expired locations (older than 48 hours, but not persistent ones)
|
|
||||||
const cleanupExpiredLocations = async () => {
|
|
||||||
console.log('Running cleanup of expired locations');
|
|
||||||
try {
|
|
||||||
const locationModel = databaseService.getLocationModel();
|
|
||||||
const result = await locationModel.cleanupExpired();
|
|
||||||
console.log(`Cleaned up ${result.changes} expired locations (persistent reports preserved)`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error cleaning up expired locations:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run cleanup every hour
|
|
||||||
console.log('Scheduling hourly cleanup task');
|
|
||||||
cron.schedule('0 * * * *', cleanupExpiredLocations);
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123'; // Change this!
|
|
||||||
|
|
||||||
// Authentication middleware
|
|
||||||
const authenticateAdmin = (req, res, next) => {
|
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
|
||||||
if (token !== ADMIN_PASSWORD) {
|
|
||||||
return res.status(401).json({ error: 'Invalid credentials' });
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup routes after database and profanity filter are initialized
|
|
||||||
function setupRoutes() {
|
|
||||||
const locationModel = databaseService.getLocationModel();
|
|
||||||
const profanityWordModel = databaseService.getProfanityWordModel();
|
|
||||||
|
|
||||||
// API Routes
|
|
||||||
app.use('/api/config', configRoutes());
|
|
||||||
app.use('/api/locations', locationRoutes(locationModel, profanityFilter));
|
|
||||||
app.use('/api/admin', adminRoutes(locationModel, profanityWordModel, profanityFilter, authenticateAdmin));
|
|
||||||
|
|
||||||
// Static page routes
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
console.log('Serving the main page');
|
|
||||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/admin', (req, res) => {
|
|
||||||
console.log('Serving the admin page');
|
|
||||||
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/privacy', (req, res) => {
|
|
||||||
console.log('Serving the privacy policy page');
|
|
||||||
res.sendFile(path.join(__dirname, 'public', 'privacy.html'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Async server startup function
|
|
||||||
async function startServer() {
|
|
||||||
try {
|
|
||||||
// Initialize database service first
|
|
||||||
await databaseService.initialize();
|
|
||||||
console.log('Database service initialized successfully');
|
|
||||||
|
|
||||||
// Initialize profanity filter
|
|
||||||
await initializeProfanityFilter();
|
|
||||||
|
|
||||||
// Validate profanity filter is properly initialized
|
|
||||||
if (!profanityFilter) {
|
|
||||||
console.error('CRITICAL ERROR: profanityFilter is undefined after initialization attempt.');
|
|
||||||
console.error('Cannot start server without a functional profanity filter.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize routes after everything is set up
|
|
||||||
setupRoutes();
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log('===========================================');
|
|
||||||
console.log('Great Lakes Ice Report server started');
|
|
||||||
console.log(`Listening on port ${PORT}`);
|
|
||||||
console.log(`Visit http://localhost:${PORT} to view the website`);
|
|
||||||
|
|
||||||
// Display profanity filter status
|
|
||||||
if (profanityFilter._isFallback) {
|
|
||||||
console.log('🚨 PROFANITY FILTER: DISABLED (FALLBACK MODE)');
|
|
||||||
console.log('⚠️ ALL USER CONTENT WILL BE ALLOWED!');
|
|
||||||
} else {
|
|
||||||
console.log('✅ PROFANITY FILTER: ACTIVE AND FUNCTIONAL');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('===========================================');
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('CRITICAL ERROR: Failed to start server:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
startServer();
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
console.log('\nShutting down server...');
|
|
||||||
|
|
||||||
// Close profanity filter database first
|
|
||||||
if (profanityFilter && typeof profanityFilter.close === 'function') {
|
|
||||||
try {
|
|
||||||
profanityFilter.close();
|
|
||||||
console.log('Profanity filter database closed.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error closing profanity filter:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close database service
|
|
||||||
databaseService.close();
|
|
||||||
console.log('Database connections closed.');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
|
@ -1,93 +0,0 @@
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const path = require('path');
|
|
||||||
const Location = require('../models/Location');
|
|
||||||
const ProfanityWord = require('../models/ProfanityWord');
|
|
||||||
|
|
||||||
class DatabaseService {
|
|
||||||
constructor() {
|
|
||||||
this.mainDb = null;
|
|
||||||
this.profanityDb = null;
|
|
||||||
this.locationModel = null;
|
|
||||||
this.profanityWordModel = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
await this.initializeMainDatabase();
|
|
||||||
await this.initializeProfanityDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeMainDatabase() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dbPath = path.join(__dirname, '..', 'icewatch.db');
|
|
||||||
this.mainDb = new sqlite3.Database(dbPath, async (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Could not connect to main database', err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
console.log('Connected to main SQLite database.');
|
|
||||||
|
|
||||||
this.locationModel = new Location(this.mainDb);
|
|
||||||
try {
|
|
||||||
await this.locationModel.initializeTable();
|
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeProfanityDatabase() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const dbPath = path.join(__dirname, '..', 'profanity.db');
|
|
||||||
this.profanityDb = new sqlite3.Database(dbPath, async (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Could not connect to profanity database', err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
console.log('Connected to profanity SQLite database.');
|
|
||||||
|
|
||||||
this.profanityWordModel = new ProfanityWord(this.profanityDb);
|
|
||||||
try {
|
|
||||||
await this.profanityWordModel.initializeTable();
|
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getLocationModel() {
|
|
||||||
if (!this.locationModel) {
|
|
||||||
throw new Error('Database not initialized. Call initialize() first.');
|
|
||||||
}
|
|
||||||
return this.locationModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
getProfanityWordModel() {
|
|
||||||
if (!this.profanityWordModel) {
|
|
||||||
throw new Error('Database not initialized. Call initialize() first.');
|
|
||||||
}
|
|
||||||
return this.profanityWordModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMainDb() {
|
|
||||||
return this.mainDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
getProfanityDb() {
|
|
||||||
return this.profanityDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (this.mainDb) {
|
|
||||||
this.mainDb.close();
|
|
||||||
}
|
|
||||||
if (this.profanityDb) {
|
|
||||||
this.profanityDb.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DatabaseService;
|
|
|
@ -1,335 +0,0 @@
|
||||||
/**
|
|
||||||
* Refactored Profanity Filter Service that uses the ProfanityWord model
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ProfanityFilterService {
|
|
||||||
constructor(profanityWordModel) {
|
|
||||||
this.profanityWordModel = profanityWordModel;
|
|
||||||
this.isInitialized = false;
|
|
||||||
|
|
||||||
// Base profanity words - comprehensive list
|
|
||||||
this.baseProfanityWords = [
|
|
||||||
// Common profanity
|
|
||||||
'damn', 'hell', 'crap', 'shit', 'fuck', 'ass', 'bitch', 'bastard',
|
|
||||||
'piss', 'whore', 'slut', 'retard', 'fag', 'gay', 'homo', 'tranny',
|
|
||||||
'dickhead', 'asshole', 'motherfucker', 'cocksucker', 'twat', 'cunt',
|
|
||||||
|
|
||||||
// Racial slurs and hate speech
|
|
||||||
'nigger', 'nigga', 'spic', 'wetback', 'chink', 'gook', 'kike',
|
|
||||||
'raghead', 'towelhead', 'beaner', 'cracker', 'honkey', 'whitey',
|
|
||||||
'kyke', 'jigaboo', 'coon', 'darkie', 'mammy', 'pickaninny',
|
|
||||||
|
|
||||||
// Sexual content
|
|
||||||
'penis', 'vagina', 'boob', 'tit', 'cock', 'dick', 'pussy', 'cum',
|
|
||||||
'sex', 'porn', 'nude', 'naked', 'horny', 'masturbate', 'orgasm',
|
|
||||||
'blowjob', 'handjob', 'anal', 'penetration', 'erection', 'climax',
|
|
||||||
|
|
||||||
// Violence and threats
|
|
||||||
'kill', 'murder', 'shoot', 'bomb', 'terrorist', 'suicide', 'rape',
|
|
||||||
'violence', 'assault', 'attack', 'threat', 'harm', 'hurt', 'pain',
|
|
||||||
'stab', 'strangle', 'torture', 'execute', 'assassinate', 'slaughter',
|
|
||||||
|
|
||||||
// Drugs and substances
|
|
||||||
'weed', 'marijuana', 'cocaine', 'heroin', 'meth', 'drugs', 'high',
|
|
||||||
'stoned', 'drunk', 'alcohol', 'beer', 'liquor', 'vodka', 'whiskey',
|
|
||||||
'ecstasy', 'lsd', 'crack', 'dope', 'pot', 'joint', 'bong',
|
|
||||||
|
|
||||||
// Religious/cultural insults
|
|
||||||
'jesus christ', 'goddamn', 'christ almighty', 'holy shit', 'god damn',
|
|
||||||
'for christ sake', 'jesus fucking christ', 'holy fuck',
|
|
||||||
|
|
||||||
// Body parts (inappropriate context)
|
|
||||||
'testicles', 'balls', 'scrotum', 'clitoris', 'labia', 'anus',
|
|
||||||
'rectum', 'butthole', 'nipples', 'breasts',
|
|
||||||
|
|
||||||
// Misc inappropriate
|
|
||||||
'wtf', 'omfg', 'stfu', 'gtfo', 'milf', 'dilf', 'thot', 'simp',
|
|
||||||
'incel', 'chad', 'beta', 'alpha male', 'mansplain', 'karen'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Leetspeak and common substitutions
|
|
||||||
this.leetMap = {
|
|
||||||
'0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '6': 'g', '7': 't',
|
|
||||||
'8': 'b', '9': 'g', '@': 'a', '$': 's', '!': 'i', '+': 't', '*': 'a',
|
|
||||||
'%': 'a', '(': 'c', ')': 'c', '&': 'a', '#': 'h', '|': 'l', '\\': '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize custom words array
|
|
||||||
this.customWords = [];
|
|
||||||
|
|
||||||
// Initialize patterns to null; will be built during async initialization
|
|
||||||
this.patterns = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the filter by loading custom words
|
|
||||||
*/
|
|
||||||
async initialize() {
|
|
||||||
if (this.isInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.loadCustomWords();
|
|
||||||
this.isInitialized = true;
|
|
||||||
console.log('ProfanityFilterService initialization completed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during ProfanityFilterService initialization:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load custom words from database using the model
|
|
||||||
*/
|
|
||||||
async loadCustomWords() {
|
|
||||||
try {
|
|
||||||
const rows = await this.profanityWordModel.loadWords();
|
|
||||||
|
|
||||||
this.customWords = rows.map(row => ({
|
|
||||||
word: row.word.toLowerCase(),
|
|
||||||
severity: row.severity,
|
|
||||||
category: row.category
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(`Loaded ${this.customWords.length} custom profanity words`);
|
|
||||||
this.patterns = this.buildPatterns(); // Rebuild patterns with custom words
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading custom profanity words:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build regex patterns for all profanity words
|
|
||||||
*/
|
|
||||||
buildPatterns() {
|
|
||||||
const allWords = [...this.baseProfanityWords, ...this.customWords.map(w => w.word)];
|
|
||||||
|
|
||||||
// Sort by length (longest first) to catch longer variations before shorter ones
|
|
||||||
allWords.sort((a, b) => b.length - a.length);
|
|
||||||
|
|
||||||
// Create patterns with word boundaries and common variations
|
|
||||||
return allWords.map(word => {
|
|
||||||
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const pattern = escaped
|
|
||||||
.split('')
|
|
||||||
.map(char => {
|
|
||||||
const leetChars = Object.entries(this.leetMap)
|
|
||||||
.filter(([_, v]) => v === char.toLowerCase())
|
|
||||||
.map(([k, _]) => k);
|
|
||||||
|
|
||||||
if (leetChars.length > 0) {
|
|
||||||
const allChars = [char, ...leetChars].map(c =>
|
|
||||||
c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
);
|
|
||||||
return `[${allChars.join('')}]`;
|
|
||||||
}
|
|
||||||
return char;
|
|
||||||
})
|
|
||||||
.join('[\\s\\-\\_\\*\\.]*');
|
|
||||||
|
|
||||||
return {
|
|
||||||
word: word,
|
|
||||||
pattern: new RegExp(`\\b${pattern}\\b`, 'gi'),
|
|
||||||
severity: this.getSeverity(word),
|
|
||||||
category: this.getCategory(word)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get severity level for a word
|
|
||||||
*/
|
|
||||||
getSeverity(word) {
|
|
||||||
// Check custom words first
|
|
||||||
const customWord = this.customWords.find(w => w.word === word.toLowerCase());
|
|
||||||
if (customWord) {
|
|
||||||
return customWord.severity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categorize severity based on type
|
|
||||||
const highSeverity = ['nigger', 'nigga', 'cunt', 'fag', 'retard', 'kike', 'spic', 'gook', 'chink'];
|
|
||||||
const lowSeverity = ['damn', 'hell', 'crap', 'wtf', 'omfg'];
|
|
||||||
|
|
||||||
if (highSeverity.includes(word.toLowerCase())) return 'high';
|
|
||||||
if (lowSeverity.includes(word.toLowerCase())) return 'low';
|
|
||||||
return 'medium';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get category for a word
|
|
||||||
*/
|
|
||||||
getCategory(word) {
|
|
||||||
// Check custom words first
|
|
||||||
const customWord = this.customWords.find(w => w.word === word.toLowerCase());
|
|
||||||
if (customWord) {
|
|
||||||
return customWord.category;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categorize based on type
|
|
||||||
const categories = {
|
|
||||||
racial: ['nigger', 'nigga', 'spic', 'wetback', 'chink', 'gook', 'kike', 'raghead', 'towelhead', 'beaner', 'cracker', 'honkey', 'whitey'],
|
|
||||||
sexual: ['penis', 'vagina', 'boob', 'tit', 'cock', 'dick', 'pussy', 'cum', 'sex', 'porn', 'nude', 'naked', 'horny', 'masturbate'],
|
|
||||||
violence: ['kill', 'murder', 'shoot', 'bomb', 'terrorist', 'suicide', 'rape', 'violence', 'assault', 'attack'],
|
|
||||||
substance: ['weed', 'marijuana', 'cocaine', 'heroin', 'meth', 'drugs', 'high', 'stoned', 'drunk', 'alcohol'],
|
|
||||||
general: ['shit', 'fuck', 'ass', 'bitch', 'bastard', 'damn', 'hell', 'crap']
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [category, words] of Object.entries(categories)) {
|
|
||||||
if (words.includes(word.toLowerCase())) {
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'general';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize text for checking
|
|
||||||
*/
|
|
||||||
normalizeText(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Convert to lowercase and handle basic substitutions
|
|
||||||
let normalized = text.toLowerCase();
|
|
||||||
|
|
||||||
// Replace multiple spaces/special chars with single space
|
|
||||||
normalized = normalized.replace(/[\s\-\_\*\.]+/g, ' ');
|
|
||||||
|
|
||||||
// Apply leet speak conversions
|
|
||||||
normalized = normalized.split('').map(char =>
|
|
||||||
this.leetMap[char] || char
|
|
||||||
).join('');
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if text contains profanity
|
|
||||||
*/
|
|
||||||
containsProfanity(text) {
|
|
||||||
if (!text || !this.patterns) return false;
|
|
||||||
|
|
||||||
const normalized = this.normalizeText(text);
|
|
||||||
return this.patterns.some(({ pattern }) => pattern.test(normalized));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze text for profanity with detailed results
|
|
||||||
*/
|
|
||||||
analyzeProfanity(text) {
|
|
||||||
if (!text || !this.patterns) {
|
|
||||||
return {
|
|
||||||
hasProfanity: false,
|
|
||||||
matches: [],
|
|
||||||
severity: 'none',
|
|
||||||
count: 0,
|
|
||||||
filtered: text || ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = this.normalizeText(text);
|
|
||||||
const matches = [];
|
|
||||||
let filteredText = text;
|
|
||||||
|
|
||||||
this.patterns.forEach(({ word, pattern, severity, category }) => {
|
|
||||||
const regex = new RegExp(pattern.source, 'gi');
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = regex.exec(normalized)) !== null) {
|
|
||||||
matches.push({
|
|
||||||
word: word,
|
|
||||||
found: match[0],
|
|
||||||
index: match.index,
|
|
||||||
severity: severity,
|
|
||||||
category: category
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace in filtered text
|
|
||||||
const replacement = '*'.repeat(match[0].length);
|
|
||||||
filteredText = filteredText.substring(0, match.index) +
|
|
||||||
replacement +
|
|
||||||
filteredText.substring(match.index + match[0].length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine overall severity
|
|
||||||
let overallSeverity = 'none';
|
|
||||||
if (matches.length > 0) {
|
|
||||||
if (matches.some(m => m.severity === 'high')) {
|
|
||||||
overallSeverity = 'high';
|
|
||||||
} else if (matches.some(m => m.severity === 'medium')) {
|
|
||||||
overallSeverity = 'medium';
|
|
||||||
} else {
|
|
||||||
overallSeverity = 'low';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasProfanity: matches.length > 0,
|
|
||||||
matches: matches,
|
|
||||||
severity: overallSeverity,
|
|
||||||
count: matches.length,
|
|
||||||
filtered: filteredText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter profanity from text
|
|
||||||
*/
|
|
||||||
filterProfanity(text, replacementChar = '*') {
|
|
||||||
const analysis = this.analyzeProfanity(text);
|
|
||||||
return analysis.filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a custom word using the model
|
|
||||||
*/
|
|
||||||
async addCustomWord(word, severity = 'medium', category = 'custom', createdBy = 'admin') {
|
|
||||||
try {
|
|
||||||
const result = await this.profanityWordModel.create(word, severity, category, createdBy);
|
|
||||||
await this.loadCustomWords(); // Reload to update patterns
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message.includes('UNIQUE constraint failed')) {
|
|
||||||
throw new Error('Word already exists in the filter');
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a custom word using the model
|
|
||||||
*/
|
|
||||||
async removeCustomWord(wordId) {
|
|
||||||
const result = await this.profanityWordModel.delete(wordId);
|
|
||||||
if (result.changes === 0) {
|
|
||||||
throw new Error('Word not found');
|
|
||||||
}
|
|
||||||
await this.loadCustomWords(); // Reload to update patterns
|
|
||||||
return { deleted: true, changes: result.changes };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all custom words using the model
|
|
||||||
*/
|
|
||||||
async getCustomWords() {
|
|
||||||
return await this.profanityWordModel.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a custom word using the model
|
|
||||||
*/
|
|
||||||
async updateCustomWord(wordId, updates) {
|
|
||||||
const { word, severity, category } = updates;
|
|
||||||
const result = await this.profanityWordModel.update(wordId, word, severity, category);
|
|
||||||
if (result.changes === 0) {
|
|
||||||
throw new Error('Word not found');
|
|
||||||
}
|
|
||||||
await this.loadCustomWords(); // Reload to update patterns
|
|
||||||
return { updated: true, changes: result.changes };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ProfanityFilterService;
|
|
Loading…
Add table
Add a link
Reference in a new issue