minor changes

This commit is contained in:
2025-11-24 16:32:13 +01:00
parent 3c23aae864
commit 6d0cada610
14 changed files with 1059 additions and 491 deletions

View File

@@ -26,6 +26,50 @@ const MAX_POST_TEXT_LENGTH = 4000;
const MIN_TEXT_HASH_LENGTH = 120;
const MAX_BOOKMARK_LABEL_LENGTH = 120;
const MAX_BOOKMARK_QUERY_LENGTH = 200;
const SPORTS_SCORING_DEFAULTS = {
enabled: 1,
threshold: 5,
auto_hide_enabled: 0,
weights: {
scoreline: 3,
scoreEmoji: 2,
sportEmoji: 2,
sportVerb: 1.5,
sportNoun: 2,
hashtag: 1.5,
teamToken: 2,
competition: 2,
celebration: 1,
location: 1
}
};
const SPORTS_SCORING_TERMS_DEFAULTS = {
nouns: [
'auswärtssieg', 'heimsieg', 'derbysieg', 'revanche', 'spiel', 'spieltag', 'match', 'derby', 'finale',
'cup', 'pokal', 'liga', 'bundesliga', 'oberliga', 'kreisliga', 'bezirksliga', 'meisterschaft',
'turnier', 'halbzeit', 'tabellenplatz', 'tabelle', 'tor', 'tore', 'treffer', 'stadion', 'arena', 'halle',
'trainerteam', 'mannschaft', 'fans', 'fanblock', 'jugend', 'u17', 'u19', 'u15'
],
verbs: [
'gewinnen', 'siegen', 'geholt', 'erkämpfen', 'erkämpft', 'erkämpfen', 'drehen', 'punkten',
'trifft', 'treffen', 'schießt', 'schiesst', 'schießen', 'schiessen', 'verteidigen', 'stürmen', 'kämpfen'
],
competitions: [
'bundesliga', 'liga', 'serie a', 'premier league', 'champions league', 'europa league', 'dfb-pokal',
'cup', 'pokal', 'qualifikation', 'qualirunde', 'halbfinale', 'viertelfinale', 'achtelfinale', 'relegation'
],
celebrations: [
'sieg', 'siege', 'auswärtssieg', 'heimsieg', 'auswärtsspiel', 'punkte', 'punkte geholt', 'man of the match', 'motm',
'tabellenführung', 'tabellenplatz', 'tabellendritter', 'tabellenzweiter'
],
locations: [
'auswärts', 'heimspiel', 'derby', 'arena', 'stadion', 'halle', 'bolle', 'bölle', 'mosel'
],
negatives: [
'rezept', 'kochen', 'politik', 'wahl', 'bundestag', 'landtag', 'software', 'release', 'update', 'konzert',
'album', 'tour', 'podcast', 'karriere', 'job', 'stellenangebot', 'bewerbung'
]
};
const screenshotDir = path.join(__dirname, 'data', 'screenshots');
if (!fs.existsSync(screenshotDir)) {
@@ -803,6 +847,21 @@ db.exec(`
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS moderation_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
sports_scoring_enabled INTEGER DEFAULT ${SPORTS_SCORING_DEFAULTS.enabled},
sports_score_threshold REAL DEFAULT ${SPORTS_SCORING_DEFAULTS.threshold},
sports_auto_hide_enabled INTEGER DEFAULT ${SPORTS_SCORING_DEFAULTS.auto_hide_enabled},
sports_score_weights TEXT,
sports_terms TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
ensureColumn('moderation_settings', 'sports_terms', 'sports_terms TEXT');
ensureColumn('moderation_settings', 'sports_auto_hide_enabled', 'sports_auto_hide_enabled INTEGER DEFAULT 0');
db.exec(`
CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at
ON search_seen_posts(last_seen_at);
@@ -894,6 +953,7 @@ ensureColumn('ai_credentials', 'auto_disabled_until', 'auto_disabled_until DATET
ensureColumn('ai_credentials', 'usage_24h_count', 'usage_24h_count INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'usage_24h_reset_at', 'usage_24h_reset_at DATETIME');
ensureColumn('search_seen_posts', 'manually_hidden', 'manually_hidden INTEGER NOT NULL DEFAULT 0');
ensureColumn('search_seen_posts', 'sports_auto_hidden', 'sports_auto_hidden INTEGER NOT NULL DEFAULT 0');
db.exec(`
CREATE TABLE IF NOT EXISTS ai_usage_events (
@@ -1620,6 +1680,152 @@ function cleanupExpiredSearchPosts() {
}
}
function safeParseSportsWeights(raw) {
if (!raw) {
return null;
}
if (typeof raw === 'object' && !Array.isArray(raw)) {
return raw;
}
if (typeof raw !== 'string') {
return null;
}
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
return null;
}
return null;
}
function normalizeSportsScoreThreshold(value) {
const parsed = parseFloat(value);
if (Number.isNaN(parsed) || parsed < 0) {
return SPORTS_SCORING_DEFAULTS.threshold;
}
return Math.min(50, Math.max(0, parsed));
}
function normalizeSportsWeights(weights) {
const defaults = SPORTS_SCORING_DEFAULTS.weights;
const normalized = {};
const source = (weights && typeof weights === 'object' && !Array.isArray(weights))
? weights
: {};
for (const key of Object.keys(defaults)) {
const raw = source[key];
const parsed = typeof raw === 'number' ? raw : parseFloat(raw);
const value = Number.isFinite(parsed) ? parsed : defaults[key];
normalized[key] = Math.max(0, Math.min(10, value));
}
return normalized;
}
function normalizeSportsTerms(terms) {
const defaults = SPORTS_SCORING_TERMS_DEFAULTS;
const result = {};
const source = (terms && typeof terms === 'object' && !Array.isArray(terms))
? terms
: {};
const normalizeList = (list, fallback) => {
const arr = Array.isArray(list) ? list : [];
const cleaned = arr
.map((entry) => {
if (typeof entry !== 'string') return '';
return entry.trim().toLowerCase();
})
.filter((entry) => entry && entry.length <= 60);
const unique = Array.from(new Set(cleaned)).slice(0, 200);
if (unique.length) {
return unique;
}
return fallback.slice();
};
for (const key of Object.keys(defaults)) {
result[key] = normalizeList(source[key], defaults[key]);
}
return result;
}
function loadModerationSettings() {
let settings = db.prepare('SELECT * FROM moderation_settings WHERE id = 1').get();
if (!settings) {
const serializedWeights = JSON.stringify(SPORTS_SCORING_DEFAULTS.weights);
const serializedTerms = JSON.stringify(SPORTS_SCORING_TERMS_DEFAULTS);
db.prepare(`
INSERT INTO moderation_settings (id, sports_scoring_enabled, sports_score_threshold, sports_auto_hide_enabled, sports_score_weights, sports_terms, updated_at)
VALUES (1, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`).run(SPORTS_SCORING_DEFAULTS.enabled, SPORTS_SCORING_DEFAULTS.threshold, SPORTS_SCORING_DEFAULTS.auto_hide_enabled, serializedWeights, serializedTerms);
settings = {
id: 1,
sports_scoring_enabled: SPORTS_SCORING_DEFAULTS.enabled,
sports_score_threshold: SPORTS_SCORING_DEFAULTS.threshold,
sports_auto_hide_enabled: SPORTS_SCORING_DEFAULTS.auto_hide_enabled,
sports_score_weights: serializedWeights,
sports_terms: serializedTerms
};
}
const weights = normalizeSportsWeights(safeParseSportsWeights(settings.sports_score_weights));
const threshold = normalizeSportsScoreThreshold(settings.sports_score_threshold);
let terms = SPORTS_SCORING_TERMS_DEFAULTS;
try {
const parsedTerms = settings.sports_terms ? JSON.parse(settings.sports_terms) : null;
terms = normalizeSportsTerms(parsedTerms);
} catch (error) {
terms = SPORTS_SCORING_TERMS_DEFAULTS;
}
return {
sports_scoring_enabled: !!settings.sports_scoring_enabled,
sports_score_threshold: threshold,
sports_auto_hide_enabled: !!settings.sports_auto_hide_enabled,
sports_score_weights: weights,
sports_terms: terms
};
}
function persistModerationSettings({ enabled, threshold, weights, terms, autoHide }) {
const normalizedEnabled = enabled ? 1 : 0;
const normalizedAutoHide = autoHide ? 1 : 0;
const normalizedThreshold = normalizeSportsScoreThreshold(threshold);
const normalizedWeights = normalizeSportsWeights(weights);
const serializedWeights = JSON.stringify(normalizedWeights);
const normalizedTerms = normalizeSportsTerms(terms);
const serializedTerms = JSON.stringify(normalizedTerms);
const existing = db.prepare('SELECT id FROM moderation_settings WHERE id = 1').get();
if (existing) {
db.prepare(`
UPDATE moderation_settings
SET sports_scoring_enabled = ?, sports_score_threshold = ?, sports_auto_hide_enabled = ?, sports_score_weights = ?, sports_terms = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = 1
`).run(normalizedEnabled, normalizedThreshold, normalizedAutoHide, serializedWeights, serializedTerms);
} else {
db.prepare(`
INSERT INTO moderation_settings (id, sports_scoring_enabled, sports_score_threshold, sports_auto_hide_enabled, sports_score_weights, sports_terms, updated_at)
VALUES (1, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`).run(normalizedEnabled, normalizedThreshold, normalizedAutoHide, serializedWeights, serializedTerms);
}
return {
sports_scoring_enabled: !!normalizedEnabled,
sports_score_threshold: normalizedThreshold,
sports_auto_hide_enabled: !!normalizedAutoHide,
sports_score_weights: normalizedWeights,
sports_terms: normalizedTerms
};
}
function expandPhotoUrlHostVariants(url) {
if (typeof url !== 'string' || !url) {
return [];
@@ -1826,14 +2032,14 @@ function removeSearchSeenEntries(urls) {
cleanupExpiredSearchPosts();
const selectSearchSeenStmt = db.prepare('SELECT url, seen_count, manually_hidden, first_seen_at, last_seen_at FROM search_seen_posts WHERE url = ?');
const selectSearchSeenStmt = db.prepare('SELECT url, seen_count, manually_hidden, sports_auto_hidden, first_seen_at, last_seen_at FROM search_seen_posts WHERE url = ?');
const insertSearchSeenStmt = db.prepare(`
INSERT INTO search_seen_posts (url, seen_count, manually_hidden, first_seen_at, last_seen_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
INSERT INTO search_seen_posts (url, seen_count, manually_hidden, sports_auto_hidden, first_seen_at, last_seen_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`);
const updateSearchSeenStmt = db.prepare(`
UPDATE search_seen_posts
SET seen_count = ?, manually_hidden = ?, last_seen_at = CURRENT_TIMESTAMP
SET seen_count = ?, manually_hidden = ?, sports_auto_hidden = ?, last_seen_at = CURRENT_TIMESTAMP
WHERE url = ?
`);
const checkIndexes = db.prepare("PRAGMA index_list('checks')").all();
@@ -2158,7 +2364,7 @@ app.get('/api/posts/by-url', (req, res) => {
app.post('/api/search-posts', (req, res) => {
try {
const { url, candidates, skip_increment, force_hide } = req.body || {};
const { url, candidates, skip_increment, force_hide, sports_auto_hide } = req.body || {};
const normalizedUrls = collectNormalizedFacebookUrls(url, candidates);
if (!normalizedUrls.length) {
@@ -2200,6 +2406,8 @@ app.post('/api/search-posts', (req, res) => {
const targetUrl = existingUrl || normalizedUrls[0];
const existingManualHidden = existingRow ? !!existingRow.manually_hidden : false;
const existingSportsHidden = existingRow ? !!existingRow.sports_auto_hidden : false;
const sportsHideRequested = !!sports_auto_hide;
if (force_hide) {
const desiredCount = Math.max(existingRow ? existingRow.seen_count : 0, SEARCH_POST_HIDE_THRESHOLD);
@@ -2208,36 +2416,38 @@ app.post('/api/search-posts', (req, res) => {
for (const candidate of urlsToUpdate) {
const row = selectSearchSeenStmt.get(candidate);
const candidateCount = row ? Math.max(row.seen_count, desiredCount) : desiredCount;
const nextSportsHidden = sportsHideRequested || (row ? !!row.sports_auto_hidden : false);
if (row) {
updateSearchSeenStmt.run(candidateCount, 1, candidate);
updateSearchSeenStmt.run(candidateCount, 1, nextSportsHidden ? 1 : 0, candidate);
} else {
insertSearchSeenStmt.run(candidate, candidateCount, 1);
insertSearchSeenStmt.run(candidate, candidateCount, 1, nextSportsHidden ? 1 : 0);
}
}
return res.json({ seen_count: desiredCount, should_hide: true, manually_hidden: true });
return res.json({ seen_count: desiredCount, should_hide: true, manually_hidden: true, sports_auto_hidden: sportsHideRequested || existingSportsHidden });
}
if (skip_increment) {
if (!existingRow) {
return res.json({ seen_count: 0, should_hide: false, manually_hidden: false });
return res.json({ seen_count: 0, should_hide: false, manually_hidden: false, sports_auto_hidden: false });
}
const seenCount = existingRow.seen_count;
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || existingManualHidden;
return res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: existingManualHidden });
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || existingManualHidden || existingSportsHidden;
return res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: existingManualHidden, sports_auto_hidden: existingSportsHidden });
}
let seenCount = existingRow ? existingRow.seen_count + 1 : 1;
const manualHidden = existingManualHidden;
const sportsHidden = sportsHideRequested || existingSportsHidden;
if (existingRow) {
updateSearchSeenStmt.run(seenCount, manualHidden ? 1 : 0, targetUrl);
updateSearchSeenStmt.run(seenCount, manualHidden ? 1 : 0, sportsHidden ? 1 : 0, targetUrl);
} else {
insertSearchSeenStmt.run(targetUrl, seenCount, manualHidden ? 1 : 0);
insertSearchSeenStmt.run(targetUrl, seenCount, manualHidden ? 1 : 0, sportsHidden ? 1 : 0);
}
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || manualHidden;
res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: manualHidden });
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || manualHidden || sportsHidden;
res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: manualHidden, sports_auto_hidden: sportsHidden });
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -3259,6 +3469,31 @@ app.put('/api/ai-settings', (req, res) => {
}
});
app.get('/api/moderation-settings', (req, res) => {
try {
const settings = loadModerationSettings();
res.json(settings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.put('/api/moderation-settings', (req, res) => {
try {
const body = req.body || {};
const saved = persistModerationSettings({
enabled: !!body.sports_scoring_enabled,
threshold: body.sports_score_threshold,
weights: body.sports_score_weights,
terms: body.sports_terms,
autoHide: !!body.sports_auto_hide_enabled
});
res.json(saved);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/hidden-settings', (req, res) => {
try {
const settings = loadHiddenSettings();