minor changes
This commit is contained in:
@@ -26,6 +26,50 @@ const MAX_POST_TEXT_LENGTH = 4000;
|
|||||||
const MIN_TEXT_HASH_LENGTH = 120;
|
const MIN_TEXT_HASH_LENGTH = 120;
|
||||||
const MAX_BOOKMARK_LABEL_LENGTH = 120;
|
const MAX_BOOKMARK_LABEL_LENGTH = 120;
|
||||||
const MAX_BOOKMARK_QUERY_LENGTH = 200;
|
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');
|
const screenshotDir = path.join(__dirname, 'data', 'screenshots');
|
||||||
if (!fs.existsSync(screenshotDir)) {
|
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(`
|
db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at
|
CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at
|
||||||
ON 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_count', 'usage_24h_count INTEGER DEFAULT 0');
|
||||||
ensureColumn('ai_credentials', 'usage_24h_reset_at', 'usage_24h_reset_at DATETIME');
|
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', '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(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS ai_usage_events (
|
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) {
|
function expandPhotoUrlHostVariants(url) {
|
||||||
if (typeof url !== 'string' || !url) {
|
if (typeof url !== 'string' || !url) {
|
||||||
return [];
|
return [];
|
||||||
@@ -1826,14 +2032,14 @@ function removeSearchSeenEntries(urls) {
|
|||||||
|
|
||||||
cleanupExpiredSearchPosts();
|
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(`
|
const insertSearchSeenStmt = db.prepare(`
|
||||||
INSERT INTO search_seen_posts (url, seen_count, manually_hidden, first_seen_at, last_seen_at)
|
INSERT INTO search_seen_posts (url, seen_count, manually_hidden, sports_auto_hidden, first_seen_at, last_seen_at)
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
`);
|
`);
|
||||||
const updateSearchSeenStmt = db.prepare(`
|
const updateSearchSeenStmt = db.prepare(`
|
||||||
UPDATE search_seen_posts
|
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 = ?
|
WHERE url = ?
|
||||||
`);
|
`);
|
||||||
const checkIndexes = db.prepare("PRAGMA index_list('checks')").all();
|
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) => {
|
app.post('/api/search-posts', (req, res) => {
|
||||||
try {
|
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);
|
const normalizedUrls = collectNormalizedFacebookUrls(url, candidates);
|
||||||
if (!normalizedUrls.length) {
|
if (!normalizedUrls.length) {
|
||||||
@@ -2200,6 +2406,8 @@ app.post('/api/search-posts', (req, res) => {
|
|||||||
|
|
||||||
const targetUrl = existingUrl || normalizedUrls[0];
|
const targetUrl = existingUrl || normalizedUrls[0];
|
||||||
const existingManualHidden = existingRow ? !!existingRow.manually_hidden : false;
|
const existingManualHidden = existingRow ? !!existingRow.manually_hidden : false;
|
||||||
|
const existingSportsHidden = existingRow ? !!existingRow.sports_auto_hidden : false;
|
||||||
|
const sportsHideRequested = !!sports_auto_hide;
|
||||||
|
|
||||||
if (force_hide) {
|
if (force_hide) {
|
||||||
const desiredCount = Math.max(existingRow ? existingRow.seen_count : 0, SEARCH_POST_HIDE_THRESHOLD);
|
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) {
|
for (const candidate of urlsToUpdate) {
|
||||||
const row = selectSearchSeenStmt.get(candidate);
|
const row = selectSearchSeenStmt.get(candidate);
|
||||||
const candidateCount = row ? Math.max(row.seen_count, desiredCount) : desiredCount;
|
const candidateCount = row ? Math.max(row.seen_count, desiredCount) : desiredCount;
|
||||||
|
const nextSportsHidden = sportsHideRequested || (row ? !!row.sports_auto_hidden : false);
|
||||||
if (row) {
|
if (row) {
|
||||||
updateSearchSeenStmt.run(candidateCount, 1, candidate);
|
updateSearchSeenStmt.run(candidateCount, 1, nextSportsHidden ? 1 : 0, candidate);
|
||||||
} else {
|
} 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 (skip_increment) {
|
||||||
if (!existingRow) {
|
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 seenCount = existingRow.seen_count;
|
||||||
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || existingManualHidden;
|
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || existingManualHidden || existingSportsHidden;
|
||||||
return res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: existingManualHidden });
|
return res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: existingManualHidden, sports_auto_hidden: existingSportsHidden });
|
||||||
}
|
}
|
||||||
|
|
||||||
let seenCount = existingRow ? existingRow.seen_count + 1 : 1;
|
let seenCount = existingRow ? existingRow.seen_count + 1 : 1;
|
||||||
const manualHidden = existingManualHidden;
|
const manualHidden = existingManualHidden;
|
||||||
|
const sportsHidden = sportsHideRequested || existingSportsHidden;
|
||||||
|
|
||||||
if (existingRow) {
|
if (existingRow) {
|
||||||
updateSearchSeenStmt.run(seenCount, manualHidden ? 1 : 0, targetUrl);
|
updateSearchSeenStmt.run(seenCount, manualHidden ? 1 : 0, sportsHidden ? 1 : 0, targetUrl);
|
||||||
} else {
|
} 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;
|
const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || manualHidden || sportsHidden;
|
||||||
res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: manualHidden });
|
res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: manualHidden, sports_auto_hidden: sportsHidden });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
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) => {
|
app.get('/api/hidden-settings', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const settings = loadHiddenSettings();
|
const settings = loadHiddenSettings();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Facebook Post Tracker Extension
|
// Facebook Post Tracker Extension
|
||||||
// Uses API_BASE_URL from config.js
|
// Uses API_BASE_URL from config.js
|
||||||
|
|
||||||
const EXTENSION_VERSION = '1.1.0';
|
const EXTENSION_VERSION = '1.2.0';
|
||||||
const PROCESSED_ATTR = 'data-fb-tracker-processed';
|
const PROCESSED_ATTR = 'data-fb-tracker-processed';
|
||||||
const PENDING_ATTR = 'data-fb-tracker-pending';
|
const PENDING_ATTR = 'data-fb-tracker-pending';
|
||||||
const DIALOG_ROOT_SELECTOR = '[role="dialog"], [data-pagelet*="Modal"], [data-pagelet="StoriesRecentStoriesFeedSection"]';
|
const DIALOG_ROOT_SELECTOR = '[role="dialog"], [data-pagelet*="Modal"], [data-pagelet="StoriesRecentStoriesFeedSection"]';
|
||||||
@@ -155,6 +155,55 @@ const aiCredentialCache = {
|
|||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
pending: null
|
pending: null
|
||||||
};
|
};
|
||||||
|
const MODERATION_SETTINGS_CACHE_TTL = 5 * 60 * 1000;
|
||||||
|
const moderationSettingsCache = {
|
||||||
|
data: null,
|
||||||
|
timestamp: 0,
|
||||||
|
pending: null
|
||||||
|
};
|
||||||
|
const SPORTS_SCORING_DEFAULTS = {
|
||||||
|
threshold: 5,
|
||||||
|
weights: {
|
||||||
|
scoreline: 3,
|
||||||
|
scoreEmoji: 2,
|
||||||
|
sportEmoji: 2,
|
||||||
|
sportVerb: 1.5,
|
||||||
|
sportNoun: 2,
|
||||||
|
hashtag: 1.5,
|
||||||
|
teamToken: 2,
|
||||||
|
competition: 2,
|
||||||
|
celebration: 1,
|
||||||
|
location: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SPORT_TERMS = {
|
||||||
|
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'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
console.log(`[FB Tracker v${EXTENSION_VERSION}] Extension loaded, API URL:`, API_URL);
|
console.log(`[FB Tracker v${EXTENSION_VERSION}] Extension loaded, API URL:`, API_URL);
|
||||||
|
|
||||||
@@ -235,6 +284,100 @@ async function fetchActiveAICredentials(forceRefresh = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeModerationSettings(payload) {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return {
|
||||||
|
sports_scoring_enabled: true,
|
||||||
|
sports_score_threshold: SPORTS_SCORING_DEFAULTS.threshold,
|
||||||
|
sports_score_weights: SPORTS_SCORING_DEFAULTS.weights,
|
||||||
|
sports_terms: DEFAULT_SPORT_TERMS,
|
||||||
|
sports_auto_hide_enabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const threshold = (() => {
|
||||||
|
const parsed = parseFloat(payload.sports_score_threshold);
|
||||||
|
if (Number.isNaN(parsed) || parsed < 0) {
|
||||||
|
return SPORTS_SCORING_DEFAULTS.threshold;
|
||||||
|
}
|
||||||
|
return Math.min(50, Math.max(0, parsed));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const weightsSource = payload.sports_score_weights && typeof payload.sports_score_weights === 'object'
|
||||||
|
? payload.sports_score_weights
|
||||||
|
: {};
|
||||||
|
const normalizedWeights = { ...SPORTS_SCORING_DEFAULTS.weights };
|
||||||
|
for (const key of Object.keys(SPORTS_SCORING_DEFAULTS.weights)) {
|
||||||
|
const raw = weightsSource[key];
|
||||||
|
const parsed = typeof raw === 'number' ? raw : parseFloat(raw);
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
normalizedWeights[key] = Math.max(0, Math.min(10, parsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeTerms = (terms) => {
|
||||||
|
const base = { ...DEFAULT_SPORT_TERMS };
|
||||||
|
const src = terms && typeof terms === 'object' ? terms : {};
|
||||||
|
const normalizeList = (list, fallback) => {
|
||||||
|
if (!Array.isArray(list)) return fallback;
|
||||||
|
const cleaned = list
|
||||||
|
.map((entry) => typeof entry === 'string' ? entry.trim().toLowerCase() : '')
|
||||||
|
.filter((entry) => entry);
|
||||||
|
const unique = Array.from(new Set(cleaned)).slice(0, 200);
|
||||||
|
return unique.length ? unique : fallback;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
nouns: normalizeList(src.nouns, base.nouns),
|
||||||
|
verbs: normalizeList(src.verbs, base.verbs),
|
||||||
|
competitions: normalizeList(src.competitions, base.competitions),
|
||||||
|
celebrations: normalizeList(src.celebrations, base.celebrations),
|
||||||
|
locations: normalizeList(src.locations, base.locations),
|
||||||
|
negatives: normalizeList(src.negatives, base.negatives)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sports_scoring_enabled: payload.sports_scoring_enabled !== false,
|
||||||
|
sports_score_threshold: threshold,
|
||||||
|
sports_score_weights: normalizedWeights,
|
||||||
|
sports_terms: normalizeTerms(payload.sports_terms),
|
||||||
|
sports_auto_hide_enabled: !!payload.sports_auto_hide_enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchModerationSettings(forceRefresh = false) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (!forceRefresh && moderationSettingsCache.data && (now - moderationSettingsCache.timestamp < MODERATION_SETTINGS_CACHE_TTL)) {
|
||||||
|
return moderationSettingsCache.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moderationSettingsCache.pending) {
|
||||||
|
try {
|
||||||
|
return await moderationSettingsCache.pending;
|
||||||
|
} catch (error) {
|
||||||
|
// fallthrough to retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moderationSettingsCache.pending = (async () => {
|
||||||
|
const response = await backendFetch(`${API_URL}/moderation-settings`);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || 'Moderations-Einstellungen konnten nicht geladen werden');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const normalized = normalizeModerationSettings(data);
|
||||||
|
moderationSettingsCache.data = normalized;
|
||||||
|
moderationSettingsCache.timestamp = Date.now();
|
||||||
|
return normalized;
|
||||||
|
})();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await moderationSettingsCache.pending;
|
||||||
|
} finally {
|
||||||
|
moderationSettingsCache.pending = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatAICredentialLabel(credential) {
|
function formatAICredentialLabel(credential) {
|
||||||
if (!credential || typeof credential !== 'object') {
|
if (!credential || typeof credential !== 'object') {
|
||||||
return 'Unbekannte AI';
|
return 'Unbekannte AI';
|
||||||
@@ -658,13 +801,14 @@ async function recordSearchResultPost(primaryUrl, allUrlCandidates = [], options
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { skipIncrement = false, forceHide = false } = options || {};
|
const { skipIncrement = false, forceHide = false, sportsAutoHide = false } = options || {};
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
url: primaryUrl,
|
url: primaryUrl,
|
||||||
candidates: Array.isArray(allUrlCandidates) ? allUrlCandidates : [],
|
candidates: Array.isArray(allUrlCandidates) ? allUrlCandidates : [],
|
||||||
skip_increment: !!skipIncrement,
|
skip_increment: !!skipIncrement,
|
||||||
force_hide: !!forceHide
|
force_hide: !!forceHide,
|
||||||
|
sports_auto_hide: !!sportsAutoHide
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await backendFetch(`${API_URL}/search-posts`, {
|
const response = await backendFetch(`${API_URL}/search-posts`, {
|
||||||
@@ -1840,15 +1984,22 @@ function extractDeadlineFromPostText(postElement) {
|
|||||||
// Check if date is valid (e.g., not 31.02.)
|
// Check if date is valid (e.g., not 31.02.)
|
||||||
if (date.getMonth() === month - 1 && date.getDate() === day) {
|
if (date.getMonth() === month - 1 && date.getDate() === day) {
|
||||||
const timeInfo = extractTimeAfterIndex(fullText, pattern.lastIndex);
|
const timeInfo = extractTimeAfterIndex(fullText, pattern.lastIndex);
|
||||||
|
const hasTime = Boolean(timeInfo);
|
||||||
if (timeInfo) {
|
if (timeInfo) {
|
||||||
date.setHours(timeInfo.hour, timeInfo.minute, 0, 0);
|
date.setHours(timeInfo.hour, timeInfo.minute, 0, 0);
|
||||||
} else if (hasInclusiveKeywordNear(fullText, matchIndex)) {
|
} else if (hasInclusiveKeywordNear(fullText, matchIndex)) {
|
||||||
date.setHours(23, 59, 0, 0);
|
date.setHours(23, 59, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasInclusiveTime = hasInclusiveKeywordNear(fullText, matchIndex);
|
||||||
|
const recordHasTime = hasTime || hasInclusiveTime;
|
||||||
|
if (hasInclusiveTime && !hasTime) {
|
||||||
|
date.setHours(23, 59, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Only add if date is in the future
|
// Only add if date is in the future
|
||||||
if (date > today) {
|
if (date > today) {
|
||||||
foundDates.push(date);
|
foundDates.push({ date, hasTime: recordHasTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1856,13 +2007,19 @@ function extractDeadlineFromPostText(postElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pattern for "12. Oktober" or "12 Oktober"
|
// Pattern for "12. Oktober" or "12 Oktober"
|
||||||
const monthPattern = /\b(\d{1,2})\.?\s*(januar|jan|februar|feb|märz|mär|maerz|april|apr|mai|juni|jun|juli|jul|august|aug|september|sep|sept|oktober|okt|november|nov|dezember|dez)\b/gi;
|
const monthPattern = /\b(\d{1,2})\.?\s*(januar|jan|februar|feb|märz|mär|maerz|april|apr|mai|juni|jun|juli|jul|august|aug|september|sep|sept|oktober|okt|november|nov|dezember|dez)\s*(\d{2,4})?\b/gi;
|
||||||
let monthMatch;
|
let monthMatch;
|
||||||
while ((monthMatch = monthPattern.exec(fullText)) !== null) {
|
while ((monthMatch = monthPattern.exec(fullText)) !== null) {
|
||||||
const day = parseInt(monthMatch[1], 10);
|
const day = parseInt(monthMatch[1], 10);
|
||||||
const monthStr = monthMatch[2].toLowerCase();
|
const monthStr = monthMatch[2].toLowerCase();
|
||||||
const month = monthNames[monthStr];
|
const month = monthNames[monthStr];
|
||||||
const year = today.getFullYear();
|
let year = today.getFullYear();
|
||||||
|
if (monthMatch[3]) {
|
||||||
|
year = parseInt(monthMatch[3], 10);
|
||||||
|
if (year < 100) {
|
||||||
|
year += 2000;
|
||||||
|
}
|
||||||
|
}
|
||||||
const matchIndex = monthMatch.index;
|
const matchIndex = monthMatch.index;
|
||||||
|
|
||||||
if (month && day >= 1 && day <= 31) {
|
if (month && day >= 1 && day <= 31) {
|
||||||
@@ -1871,30 +2028,246 @@ function extractDeadlineFromPostText(postElement) {
|
|||||||
// Check if date is valid
|
// Check if date is valid
|
||||||
if (date.getMonth() === month - 1 && date.getDate() === day) {
|
if (date.getMonth() === month - 1 && date.getDate() === day) {
|
||||||
const timeInfo = extractTimeAfterIndex(fullText, monthPattern.lastIndex);
|
const timeInfo = extractTimeAfterIndex(fullText, monthPattern.lastIndex);
|
||||||
|
const hasTime = Boolean(timeInfo);
|
||||||
if (timeInfo) {
|
if (timeInfo) {
|
||||||
date.setHours(timeInfo.hour, timeInfo.minute, 0, 0);
|
date.setHours(timeInfo.hour, timeInfo.minute, 0, 0);
|
||||||
} else if (hasInclusiveKeywordNear(fullText, matchIndex)) {
|
} else if (hasInclusiveKeywordNear(fullText, matchIndex)) {
|
||||||
date.setHours(23, 59, 0, 0);
|
date.setHours(23, 59, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasInclusiveTime = hasInclusiveKeywordNear(fullText, matchIndex);
|
||||||
|
const recordHasTime = hasTime || hasInclusiveTime;
|
||||||
|
if (hasInclusiveTime && !hasTime) {
|
||||||
|
date.setHours(23, 59, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// If date has passed this year, assume next year
|
// If date has passed this year, assume next year
|
||||||
if (date <= today) {
|
if (date <= today) {
|
||||||
date.setFullYear(year + 1);
|
date.setFullYear(year + 1);
|
||||||
}
|
}
|
||||||
foundDates.push(date);
|
foundDates.push({ date, hasTime: recordHasTime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the earliest future date
|
// Return the earliest future date
|
||||||
if (foundDates.length > 0) {
|
if (foundDates.length > 0) {
|
||||||
foundDates.sort((a, b) => a - b);
|
foundDates.sort((a, b) => {
|
||||||
return toDateTimeLocalString(foundDates[0]);
|
const diff = a.date - b.date;
|
||||||
|
if (diff !== 0) {
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
if (a.hasTime && !b.hasTime) return -1;
|
||||||
|
if (!a.hasTime && b.hasTime) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return toDateTimeLocalString(foundDates[0].date);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeRegex(value) {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectKeywordMatches(keywords, text, limit = 20) {
|
||||||
|
if (!Array.isArray(keywords) || !keywords.length || !text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const found = [];
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (!keyword) continue;
|
||||||
|
const pattern = new RegExp(`\\b${escapeRegex(keyword)}\\b`, 'gi');
|
||||||
|
const matches = text.match(pattern);
|
||||||
|
if (matches && matches.length) {
|
||||||
|
found.push(keyword);
|
||||||
|
if (found.length >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(found));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRegexMatches(regex, text, limit = 20) {
|
||||||
|
if (!regex || !(regex instanceof RegExp) || !text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const matches = Array.from(text.matchAll(regex)).map((m) => m[0]);
|
||||||
|
if (!matches.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(new Set(matches)).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterScorelines(candidates = []) {
|
||||||
|
const filtered = [];
|
||||||
|
for (const raw of candidates) {
|
||||||
|
const parts = raw.split(':').map((part) => part.trim());
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [a, b] = parts.map((p) => parseInt(p, 10));
|
||||||
|
if (Number.isNaN(a) || Number.isNaN(b)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a < 0 || b < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a > 15 || b > 15) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filtered.push(`${a}:${b}`);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateSportsScore(text, moderationSettings = null) {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedText = text.toLowerCase();
|
||||||
|
const weights = {
|
||||||
|
...SPORTS_SCORING_DEFAULTS.weights,
|
||||||
|
...(moderationSettings && moderationSettings.sports_score_weights ? moderationSettings.sports_score_weights : {})
|
||||||
|
};
|
||||||
|
const threshold = moderationSettings && typeof moderationSettings.sports_score_threshold === 'number'
|
||||||
|
? moderationSettings.sports_score_threshold
|
||||||
|
: SPORTS_SCORING_DEFAULTS.threshold;
|
||||||
|
const terms = (() => {
|
||||||
|
const base = DEFAULT_SPORT_TERMS;
|
||||||
|
const incoming = moderationSettings && moderationSettings.sports_terms ? moderationSettings.sports_terms : null;
|
||||||
|
const normalizeList = (list, fallback) => {
|
||||||
|
if (!Array.isArray(list)) return fallback;
|
||||||
|
const cleaned = list
|
||||||
|
.map((entry) => typeof entry === 'string' ? entry.trim().toLowerCase() : '')
|
||||||
|
.filter((entry) => entry);
|
||||||
|
const unique = Array.from(new Set(cleaned)).slice(0, 200);
|
||||||
|
return unique.length ? unique : fallback;
|
||||||
|
};
|
||||||
|
const src = incoming && typeof incoming === 'object' ? incoming : {};
|
||||||
|
return {
|
||||||
|
nouns: normalizeList(src.nouns, base.nouns),
|
||||||
|
verbs: normalizeList(src.verbs, base.verbs),
|
||||||
|
competitions: normalizeList(src.competitions, base.competitions),
|
||||||
|
celebrations: normalizeList(src.celebrations, base.celebrations),
|
||||||
|
locations: normalizeList(src.locations, base.locations),
|
||||||
|
negatives: normalizeList(src.negatives, base.negatives)
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const matchesCount = (regex) => {
|
||||||
|
if (!regex || !(regex instanceof RegExp)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const matches = normalizedText.match(regex);
|
||||||
|
return matches ? matches.length : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyWeight = (count, weight, label, matches = []) => {
|
||||||
|
if (!count || !weight) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const effective = Math.min(count, 5);
|
||||||
|
const gain = effective * weight;
|
||||||
|
if (matches && matches.length) {
|
||||||
|
hitDetails.push(`${label}: ${matches.slice(0, 10).join(', ')}`);
|
||||||
|
} else {
|
||||||
|
hitDetails.push(`${label} x${effective} (+${gain.toFixed(1)})`);
|
||||||
|
}
|
||||||
|
score += gain;
|
||||||
|
return gain;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hitDetails = [];
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
const scorelineMatchesRaw = collectRegexMatches(/\b\d{1,2}\s*:\s*\d{1,2}\b/g, normalizedText);
|
||||||
|
const scorelineMatches = filterScorelines(scorelineMatchesRaw);
|
||||||
|
applyWeight(scorelineMatches.length, weights.scoreline, 'Ergebnis', scorelineMatches);
|
||||||
|
|
||||||
|
const scoreEmojiMatches = collectRegexMatches(/[0-9]️⃣/g, normalizedText)
|
||||||
|
.concat(collectRegexMatches(/\+\s*\d\b/g, normalizedText));
|
||||||
|
applyWeight(scoreEmojiMatches.length, weights.scoreEmoji, 'Punkte', scoreEmojiMatches);
|
||||||
|
|
||||||
|
const sportEmojiMatches = collectRegexMatches(/[⚽🏐🏀🏈🎾🏉🥅🏒🏑🏓🏸🤾🏏🎽🎳🥊🥋⛳]/g, normalizedText);
|
||||||
|
applyWeight(sportEmojiMatches.length, weights.sportEmoji, 'Sport-Emoji', sportEmojiMatches);
|
||||||
|
|
||||||
|
const verbMatches = collectKeywordMatches(terms.verbs, normalizedText);
|
||||||
|
applyWeight(verbMatches.length, weights.sportVerb, 'Sport-Verben', verbMatches);
|
||||||
|
|
||||||
|
const nounMatches = collectKeywordMatches(terms.nouns, normalizedText);
|
||||||
|
applyWeight(nounMatches.length, weights.sportNoun, 'Sport-Vokabeln', nounMatches);
|
||||||
|
|
||||||
|
const hashtagMatches = collectRegexMatches(/#(?:auswärtssieg|heimsieg|derbysieg|bundesliga|liga|pokal|cup|fc[a-z0-9]+|sv[a-z0-9]+|tsv[a-z0-9]+|sg[a-z0-9]+)/g, normalizedText);
|
||||||
|
applyWeight(hashtagMatches.length, weights.hashtag, 'Sport-Hashtags', hashtagMatches);
|
||||||
|
|
||||||
|
const teamMatches = collectRegexMatches(/\b(?:fc|sv|tsv|ssv|bvb|sge|fcb|hsv|vfb|fsv|sg|scl|djk)[\s\-]?[a-zäöüß0-9]+/gi, normalizedText);
|
||||||
|
applyWeight(teamMatches.length, weights.teamToken, 'Team-Kürzel', teamMatches);
|
||||||
|
|
||||||
|
const competitionMatches = collectKeywordMatches(terms.competitions, normalizedText);
|
||||||
|
applyWeight(competitionMatches.length, weights.competition, 'Liga/Turnier', competitionMatches);
|
||||||
|
|
||||||
|
const celebrationMatches = collectKeywordMatches(terms.celebrations, normalizedText);
|
||||||
|
applyWeight(celebrationMatches.length, weights.celebration, 'Ergebnisbezug', celebrationMatches);
|
||||||
|
|
||||||
|
const locationMatches = collectKeywordMatches(terms.locations, normalizedText);
|
||||||
|
applyWeight(locationMatches.length, weights.location, 'Spielort', locationMatches);
|
||||||
|
|
||||||
|
const nonSportMatches = collectKeywordMatches(terms.negatives, normalizedText);
|
||||||
|
const nonSportHits = nonSportMatches.length;
|
||||||
|
if (nonSportHits) {
|
||||||
|
const penalty = Math.min(2, nonSportHits) * 1;
|
||||||
|
score -= penalty;
|
||||||
|
hitDetails.push(`Gegenindizien: ${nonSportMatches.slice(0, 10).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalScore = Math.round(score * 10) / 10;
|
||||||
|
return {
|
||||||
|
score: finalScore,
|
||||||
|
threshold,
|
||||||
|
wouldHide: finalScore >= threshold,
|
||||||
|
hits: hitDetails
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSportsScoreBadge(scoreInfo) {
|
||||||
|
if (!scoreInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof scoreInfo.score !== 'number' || scoreInfo.score >= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
const wouldHide = !!scoreInfo.wouldHide;
|
||||||
|
const bg = wouldHide ? 'rgba(245, 158, 11, 0.18)' : 'rgba(59, 130, 246, 0.12)';
|
||||||
|
const border = wouldHide ? 'rgba(245, 158, 11, 0.5)' : 'rgba(59, 130, 246, 0.35)';
|
||||||
|
const color = wouldHide ? '#b45309' : '#1d4ed8';
|
||||||
|
badge.className = 'fb-tracker-score-badge';
|
||||||
|
badge.textContent = `Sport-Score ${scoreInfo.score.toFixed(1)} / ${scoreInfo.threshold}`;
|
||||||
|
if (scoreInfo.hits && scoreInfo.hits.length) {
|
||||||
|
const lines = scoreInfo.hits.map((hit) => `• ${hit}`).join('\n');
|
||||||
|
badge.title = `${lines}\n${wouldHide ? '≥ Schwellwert' : '< Schwellwert'}`;
|
||||||
|
} else {
|
||||||
|
badge.title = wouldHide ? 'Über Schwellwert' : 'Unter Schwellwert';
|
||||||
|
}
|
||||||
|
badge.style.cssText = `
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: ${bg};
|
||||||
|
border: 1px solid ${border};
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${color};
|
||||||
|
font-size: 12px;
|
||||||
|
`;
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFacebookPostUrl(rawValue) {
|
function normalizeFacebookPostUrl(rawValue) {
|
||||||
if (typeof rawValue !== 'string') {
|
if (typeof rawValue !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
@@ -2460,6 +2833,48 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
await addAICommentButton(container, postElement);
|
await addAICommentButton(container, postElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sportsScoreInfo = null;
|
||||||
|
try {
|
||||||
|
const moderationSettings = await fetchModerationSettings();
|
||||||
|
if (moderationSettings && moderationSettings.sports_scoring_enabled !== false) {
|
||||||
|
const postTextForScore = extractPostText(postElement);
|
||||||
|
if (postTextForScore) {
|
||||||
|
sportsScoreInfo = evaluateSportsScore(postTextForScore, moderationSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
moderationSettings.sports_auto_hide_enabled
|
||||||
|
&& sportsScoreInfo
|
||||||
|
&& sportsScoreInfo.wouldHide
|
||||||
|
&& !isTracked
|
||||||
|
&& !likedByCurrentUser
|
||||||
|
) {
|
||||||
|
if (isDialogContext) {
|
||||||
|
console.log('[FB Tracker] Post #' + postNum + ' - Would auto-hide by sports score but skipping in dialog context');
|
||||||
|
} else {
|
||||||
|
console.log('[FB Tracker] Post #' + postNum + ' - Auto-hidden by sports score', sportsScoreInfo);
|
||||||
|
try {
|
||||||
|
await recordSearchResultPost(postUrlData.url, postUrlData.allCandidates, { forceHide: true, sportsAutoHide: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('[FB Tracker] Auto-hide scoring could not persist hide state:', error);
|
||||||
|
}
|
||||||
|
hidePostElement(postElement);
|
||||||
|
processedPostUrls.set(encodedUrl, {
|
||||||
|
element: postElement,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
hidden: true,
|
||||||
|
searchSeenCount: searchTrackingInfo && typeof searchTrackingInfo.seen_count === 'number'
|
||||||
|
? searchTrackingInfo.seen_count
|
||||||
|
: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('[FB Tracker] Sport-Scoring nicht verfügbar:', error);
|
||||||
|
}
|
||||||
|
|
||||||
if (isSearchResult) {
|
if (isSearchResult) {
|
||||||
const info = document.createElement('button');
|
const info = document.createElement('button');
|
||||||
info.type = 'button';
|
info.type = 'button';
|
||||||
@@ -2596,6 +3011,15 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
|
|||||||
resetHover();
|
resetHover();
|
||||||
|
|
||||||
container.insertBefore(info, container.firstChild);
|
container.insertBefore(info, container.firstChild);
|
||||||
|
const sportsScoreBadge = buildSportsScoreBadge(sportsScoreInfo);
|
||||||
|
if (sportsScoreBadge) {
|
||||||
|
container.insertBefore(sportsScoreBadge, info.nextSibling);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sportsScoreBadge = buildSportsScoreBadge(sportsScoreInfo);
|
||||||
|
if (sportsScoreBadge) {
|
||||||
|
container.insertBefore(sportsScoreBadge, container.firstChild);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert UI - try multiple strategies to find stable insertion point
|
// Insert UI - try multiple strategies to find stable insertion point
|
||||||
@@ -3900,6 +4324,18 @@ async function setCommentText(inputElement, text, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeAIComment(comment) {
|
||||||
|
if (!comment || typeof comment !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = comment;
|
||||||
|
const sanitized = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
|
||||||
|
return sanitized.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate AI comment for a post
|
* Generate AI comment for a post
|
||||||
*/
|
*/
|
||||||
@@ -3928,7 +4364,13 @@ async function generateAIComment(postText, profileNumber, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.comment;
|
const sanitizedComment = sanitizeAIComment(data.comment);
|
||||||
|
|
||||||
|
if (!sanitizedComment) {
|
||||||
|
throw new Error('AI-Antwort enthält keinen gültigen Text');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizedComment;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[FB Tracker] AI comment generation failed:', error);
|
console.error('[FB Tracker] AI comment generation failed:', error);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Facebook Post Tracker",
|
"name": "Facebook Post Tracker",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"description": "Track Facebook posts across multiple profiles",
|
"description": "Track Facebook posts across multiple profiles",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
446
fb_feed.txt
446
fb_feed.txt
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
noScreenshot.png
BIN
noScreenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
121
web/index.html
121
web/index.html
@@ -480,6 +480,127 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Sports scoring Section -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2 class="section-title">🏷️ Sport-Post-Scoring</h2>
|
||||||
|
<p class="section-description">
|
||||||
|
Analysiert Beitragstexte nach Sport-Begriffen (z.B. Fußball, Volleyball) und weist einen Score zu.
|
||||||
|
Beiträge oberhalb des Schwellwerts würden später automatisch ausgeblendet – aktuell wird nur markiert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form id="moderationSettingsForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
<input type="checkbox" id="sportsScoringEnabled" class="form-checkbox">
|
||||||
|
<span>Scoring aktivieren</span>
|
||||||
|
</label>
|
||||||
|
<p class="form-help">
|
||||||
|
Nutze das heuristische Punktesystem, um offensichtliche Sport-/Spiel-Posts zu erkennen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sportsScoreThreshold" class="form-label">Schwellwert für Sport-Posts</label>
|
||||||
|
<input type="number" id="sportsScoreThreshold" class="form-input" min="0" max="50" step="0.5" value="5">
|
||||||
|
<p class="form-help">
|
||||||
|
Ab diesem Score kann der Post automatisch versteckt werden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
<input type="checkbox" id="sportsAutoHideEnabled" class="form-checkbox">
|
||||||
|
<span>Automatisch verstecken bei Überschreitung</span>
|
||||||
|
</label>
|
||||||
|
<p class="form-help">
|
||||||
|
Wenn aktiviert, blendet die Extension Posts mit Score ≥ Schwellwert automatisch aus (Feed & Suche).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Gewichte (0–10)</label>
|
||||||
|
<div class="grid-weights">
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Ergebnis (1:0)</span>
|
||||||
|
<input type="number" id="sportWeightScoreline" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Score-Emojis (+3️⃣)</span>
|
||||||
|
<input type="number" id="sportWeightScoreEmoji" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Sport-Emojis (⚽️)</span>
|
||||||
|
<input type="number" id="sportWeightSportEmoji" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Verben (gewinnen)</span>
|
||||||
|
<input type="number" id="sportWeightSportVerb" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Nomen (Liga, Tor)</span>
|
||||||
|
<input type="number" id="sportWeightSportNoun" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Hashtags (#auswärtssieg)</span>
|
||||||
|
<input type="number" id="sportWeightHashtag" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Team-Kürzel (FC…)</span>
|
||||||
|
<input type="number" id="sportWeightTeamToken" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Wettbewerbe (Cup)</span>
|
||||||
|
<input type="number" id="sportWeightCompetition" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Ergebnisbezug (Sieg)</span>
|
||||||
|
<input type="number" id="sportWeightCelebration" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Ort (Auswärts)</span>
|
||||||
|
<input type="number" id="sportWeightLocation" class="form-input" min="0" max="10" step="0.5">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="form-help">
|
||||||
|
Je höher das Gewicht, desto stärker zahlt der jeweilige Treffer auf den Sport-Score ein.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Stichwort-Listen (kommagetrennt)</label>
|
||||||
|
<div class="grid-weights">
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Nomen</span>
|
||||||
|
<textarea id="sportTermsNouns" class="form-textarea" rows="2" placeholder="auswärtssieg, liga, tor ..."></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Verben</span>
|
||||||
|
<textarea id="sportTermsVerbs" class="form-textarea" rows="2" placeholder="gewinnen, punkten ..."></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Wettbewerbe</span>
|
||||||
|
<textarea id="sportTermsCompetitions" class="form-textarea" rows="2" placeholder="bundesliga, cup ..."></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Ergebnisbezug</span>
|
||||||
|
<textarea id="sportTermsCelebrations" class="form-textarea" rows="2" placeholder="sieg, tabellenführung ..."></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Orte</span>
|
||||||
|
<textarea id="sportTermsLocations" class="form-textarea" rows="2" placeholder="auswärts, stadion ..."></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="form-field-inline">
|
||||||
|
<span>Negativliste</span>
|
||||||
|
<textarea id="sportTermsNegatives" class="form-textarea" rows="2" placeholder="rezept, politik ..."></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="form-help">
|
||||||
|
Leere Felder nutzen die Standardliste. Negativliste senkt den Score bei Treffern.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Hidden posts / purge settings -->
|
<!-- Hidden posts / purge settings -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2 class="section-title">Versteckte Beiträge bereinigen</h2>
|
<h2 class="section-title">Versteckte Beiträge bereinigen</h2>
|
||||||
|
|||||||
@@ -95,6 +95,20 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-weights {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-inline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1c1e21;
|
||||||
|
}
|
||||||
|
|
||||||
.form-help a {
|
.form-help a {
|
||||||
color: #1877f2;
|
color: #1877f2;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
220
web/settings.js
220
web/settings.js
@@ -44,6 +44,33 @@ const PROVIDER_INFO = {
|
|||||||
let credentials = [];
|
let credentials = [];
|
||||||
let currentSettings = null;
|
let currentSettings = null;
|
||||||
let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 };
|
let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 };
|
||||||
|
const SPORT_WEIGHT_FIELDS = [
|
||||||
|
{ key: 'scoreline', id: 'sportWeightScoreline' },
|
||||||
|
{ key: 'scoreEmoji', id: 'sportWeightScoreEmoji' },
|
||||||
|
{ key: 'sportEmoji', id: 'sportWeightSportEmoji' },
|
||||||
|
{ key: 'sportVerb', id: 'sportWeightSportVerb' },
|
||||||
|
{ key: 'sportNoun', id: 'sportWeightSportNoun' },
|
||||||
|
{ key: 'hashtag', id: 'sportWeightHashtag' },
|
||||||
|
{ key: 'teamToken', id: 'sportWeightTeamToken' },
|
||||||
|
{ key: 'competition', id: 'sportWeightCompetition' },
|
||||||
|
{ key: 'celebration', id: 'sportWeightCelebration' },
|
||||||
|
{ key: 'location', id: 'sportWeightLocation' }
|
||||||
|
];
|
||||||
|
const SPORT_TERM_FIELDS = [
|
||||||
|
{ key: 'nouns', id: 'sportTermsNouns', placeholder: 'auswärtssieg, liga, tor ...' },
|
||||||
|
{ key: 'verbs', id: 'sportTermsVerbs', placeholder: 'gewinnen, punkten ...' },
|
||||||
|
{ key: 'competitions', id: 'sportTermsCompetitions', placeholder: 'bundesliga, cup ...' },
|
||||||
|
{ key: 'celebrations', id: 'sportTermsCelebrations', placeholder: 'sieg, tabellenführung ...' },
|
||||||
|
{ key: 'locations', id: 'sportTermsLocations', placeholder: 'auswärts, stadion ...' },
|
||||||
|
{ key: 'negatives', id: 'sportTermsNegatives', placeholder: 'rezept, politik ...' }
|
||||||
|
];
|
||||||
|
let moderationSettings = {
|
||||||
|
sports_scoring_enabled: true,
|
||||||
|
sports_score_threshold: 5,
|
||||||
|
sports_score_weights: {},
|
||||||
|
sports_terms: {},
|
||||||
|
sports_auto_hide_enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
function apiFetch(url, options = {}) {
|
function apiFetch(url, options = {}) {
|
||||||
return fetch(url, {...options, credentials: 'include'});
|
return fetch(url, {...options, credentials: 'include'});
|
||||||
@@ -213,6 +240,153 @@ async function purgeHiddenNow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSportsScoreThresholdInput(value) {
|
||||||
|
const parsed = parseFloat(value);
|
||||||
|
if (Number.isNaN(parsed) || parsed < 0) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
return Math.min(50, Math.max(0, parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSportWeightInput(value, fallback = 1) {
|
||||||
|
const parsed = parseFloat(value);
|
||||||
|
if (Number.isNaN(parsed) || parsed < 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Math.min(10, Math.max(0, parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWeightInputs(enabled) {
|
||||||
|
SPORT_WEIGHT_FIELDS.forEach(({ id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) {
|
||||||
|
input.disabled = !enabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTermInputs(enabled) {
|
||||||
|
SPORT_TERM_FIELDS.forEach(({ id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) {
|
||||||
|
input.disabled = !enabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeTermListInput(value) {
|
||||||
|
if (!value || typeof value !== 'string') return [];
|
||||||
|
return value
|
||||||
|
.split(',')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry)
|
||||||
|
.slice(0, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTermList(list) {
|
||||||
|
if (!Array.isArray(list) || !list.length) return '';
|
||||||
|
return list.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyModerationSettingsUI() {
|
||||||
|
const enabledToggle = document.getElementById('sportsScoringEnabled');
|
||||||
|
const thresholdInput = document.getElementById('sportsScoreThreshold');
|
||||||
|
const autoHideToggle = document.getElementById('sportsAutoHideEnabled');
|
||||||
|
if (enabledToggle) {
|
||||||
|
enabledToggle.checked = !!moderationSettings.sports_scoring_enabled;
|
||||||
|
}
|
||||||
|
if (thresholdInput) {
|
||||||
|
thresholdInput.value = moderationSettings.sports_score_threshold ?? 5;
|
||||||
|
thresholdInput.disabled = !enabledToggle?.checked;
|
||||||
|
}
|
||||||
|
if (autoHideToggle) {
|
||||||
|
autoHideToggle.checked = !!moderationSettings.sports_auto_hide_enabled;
|
||||||
|
autoHideToggle.disabled = !enabledToggle?.checked;
|
||||||
|
}
|
||||||
|
SPORT_WEIGHT_FIELDS.forEach(({ key, id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) {
|
||||||
|
const value = moderationSettings.sports_score_weights?.[key];
|
||||||
|
input.value = typeof value === 'number' ? value : '';
|
||||||
|
input.disabled = !enabledToggle?.checked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SPORT_TERM_FIELDS.forEach(({ key, id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) {
|
||||||
|
input.value = renderTermList(moderationSettings.sports_terms?.[key]);
|
||||||
|
input.disabled = !enabledToggle?.checked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModerationSettings() {
|
||||||
|
const res = await apiFetch(`${API_URL}/moderation-settings`);
|
||||||
|
if (!res.ok) throw new Error('Konnte Moderations-Einstellungen nicht laden');
|
||||||
|
const data = await res.json();
|
||||||
|
moderationSettings = {
|
||||||
|
sports_scoring_enabled: !!data.sports_scoring_enabled,
|
||||||
|
sports_score_threshold: normalizeSportsScoreThresholdInput(data.sports_score_threshold),
|
||||||
|
sports_score_weights: data.sports_score_weights || {},
|
||||||
|
sports_terms: data.sports_terms || {},
|
||||||
|
sports_auto_hide_enabled: !!data.sports_auto_hide_enabled
|
||||||
|
};
|
||||||
|
applyModerationSettingsUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveModerationSettings(event, { silent = false } = {}) {
|
||||||
|
if (event && typeof event.preventDefault === 'function') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
const enabledToggle = document.getElementById('sportsScoringEnabled');
|
||||||
|
const thresholdInput = document.getElementById('sportsScoreThreshold');
|
||||||
|
const autoHideToggle = document.getElementById('sportsAutoHideEnabled');
|
||||||
|
const enabled = enabledToggle ? enabledToggle.checked : true;
|
||||||
|
const threshold = thresholdInput
|
||||||
|
? normalizeSportsScoreThresholdInput(thresholdInput.value)
|
||||||
|
: moderationSettings.sports_score_threshold;
|
||||||
|
const autoHide = autoHideToggle ? autoHideToggle.checked : false;
|
||||||
|
const weights = {};
|
||||||
|
SPORT_WEIGHT_FIELDS.forEach(({ key, id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
weights[key] = normalizeSportWeightInput(input ? input.value : moderationSettings.sports_score_weights?.[key], moderationSettings.sports_score_weights?.[key] ?? 1);
|
||||||
|
});
|
||||||
|
const terms = {};
|
||||||
|
SPORT_TERM_FIELDS.forEach(({ key, id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
terms[key] = serializeTermListInput(input ? input.value : moderationSettings.sports_terms?.[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`${API_URL}/moderation-settings`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sports_scoring_enabled: enabled,
|
||||||
|
sports_score_threshold: threshold,
|
||||||
|
sports_auto_hide_enabled: autoHide,
|
||||||
|
sports_score_weights: weights,
|
||||||
|
sports_terms: terms
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.error || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
|
moderationSettings = await res.json();
|
||||||
|
applyModerationSettingsUI();
|
||||||
|
if (!silent) {
|
||||||
|
showSuccess('✅ Sport-Scoring gespeichert');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (!silent) {
|
||||||
|
showError('❌ ' + err.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function shorten(text, maxLength = 80) {
|
function shorten(text, maxLength = 80) {
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
@@ -848,6 +1022,7 @@ async function saveAllSettings(event) {
|
|||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
saveSettings(null, { silent: true }),
|
saveSettings(null, { silent: true }),
|
||||||
saveHiddenSettings(null, { silent: true }),
|
saveHiddenSettings(null, { silent: true }),
|
||||||
|
saveModerationSettings(null, { silent: true }),
|
||||||
saveAllFriends({ silent: true })
|
saveAllFriends({ silent: true })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -978,6 +1153,49 @@ if (autoPurgeHiddenToggle) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sportsScoringToggle = document.getElementById('sportsScoringEnabled');
|
||||||
|
const sportsScoreInput = document.getElementById('sportsScoreThreshold');
|
||||||
|
if (sportsScoringToggle && sportsScoreInput) {
|
||||||
|
sportsScoringToggle.addEventListener('change', () => {
|
||||||
|
sportsScoreInput.disabled = !sportsScoringToggle.checked;
|
||||||
|
applyWeightInputs(sportsScoringToggle.checked);
|
||||||
|
applyTermInputs(sportsScoringToggle.checked);
|
||||||
|
const autoHideToggle = document.getElementById('sportsAutoHideEnabled');
|
||||||
|
if (autoHideToggle) {
|
||||||
|
autoHideToggle.disabled = !sportsScoringToggle.checked;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sportsScoreInput.addEventListener('blur', () => {
|
||||||
|
sportsScoreInput.value = normalizeSportsScoreThresholdInput(sportsScoreInput.value);
|
||||||
|
});
|
||||||
|
SPORT_WEIGHT_FIELDS.forEach(({ id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
input.value = normalizeSportWeightInput(input.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SPORT_TERM_FIELDS.forEach(({ id }) => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
input.value = renderTermList(serializeTermListInput(input.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const moderationForm = document.getElementById('moderationSettingsForm');
|
||||||
|
if (moderationForm) {
|
||||||
|
moderationForm.addEventListener('submit', (e) => saveModerationSettings(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
Promise.all([loadCredentials(), loadSettings(), loadHiddenSettings(), loadProfileFriends()]).catch(err => showError(err.message));
|
Promise.all([
|
||||||
|
loadCredentials(),
|
||||||
|
loadSettings(),
|
||||||
|
loadHiddenSettings(),
|
||||||
|
loadModerationSettings(),
|
||||||
|
loadProfileFriends()
|
||||||
|
]).catch(err => showError(err.message));
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user