From 66221f27c71c182a56bce4a725e45678540dcacc Mon Sep 17 00:00:00 2001
From: Meik
Date: Tue, 7 Apr 2026 15:34:18 +0200
Subject: [PATCH] Add per-profile AI comment action limits
---
backend/server.js | 225 ++++++++++++++++++++++++++++++++++++++++++++--
web/index.html | 8 ++
web/settings.js | 103 ++++++++++++++++++++-
3 files changed, 329 insertions(+), 7 deletions(-)
diff --git a/backend/server.js b/backend/server.js
index e91ea5b..06dca9f 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -11,6 +11,8 @@ const app = express();
const PORT = process.env.PORT || 3000;
const MAX_PROFILES = 5;
+const AI_AUTO_COMMENT_SOURCE = 'extension-ai-button';
+const AI_AUTO_COMMENT_DAILY_LIMIT_MAX = 500;
const DEFAULT_PROFILE_NAMES = {
1: 'Profil 1',
2: 'Profil 2',
@@ -809,6 +811,167 @@ function getProfileName(profileNumber) {
return DEFAULT_PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`;
}
+function normalizeAIAutoCommentDailyLimit(value) {
+ if (value === null || typeof value === 'undefined' || value === '') {
+ return 0;
+ }
+ const parsed = parseInt(value, 10);
+ if (Number.isNaN(parsed) || parsed < 0) {
+ return 0;
+ }
+ return Math.min(AI_AUTO_COMMENT_DAILY_LIMIT_MAX, parsed);
+}
+
+function getLocalDateKey(date = new Date()) {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+}
+
+function getNextLocalMidnightIso(date = new Date()) {
+ const nextMidnight = new Date(date);
+ nextMidnight.setHours(24, 0, 0, 0);
+ return nextMidnight.toISOString();
+}
+
+function buildAIAutoCommentLimitPayload(profileNumber, dailyLimit, usedToday, date = new Date()) {
+ const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
+ if (!normalizedProfileNumber) {
+ return null;
+ }
+ const normalizedDailyLimit = normalizeAIAutoCommentDailyLimit(dailyLimit);
+ const normalizedUsedToday = Math.max(0, parseInt(usedToday, 10) || 0);
+ return {
+ profile_number: normalizedProfileNumber,
+ profile_name: getProfileName(normalizedProfileNumber),
+ daily_limit: normalizedDailyLimit,
+ used_today: normalizedUsedToday,
+ remaining_today: normalizedDailyLimit > 0
+ ? Math.max(0, normalizedDailyLimit - normalizedUsedToday)
+ : null,
+ blocked: normalizedDailyLimit > 0 && normalizedUsedToday >= normalizedDailyLimit,
+ resets_at: getNextLocalMidnightIso(date)
+ };
+}
+
+function listAIAutoCommentProfileLimits() {
+ const todayKey = getLocalDateKey();
+ const limitRows = db.prepare(`
+ SELECT profile_number, daily_limit
+ FROM ai_profile_auto_comment_limits
+ `).all();
+ const usageRows = db.prepare(`
+ SELECT profile_number, used_count
+ FROM ai_profile_auto_comment_usage
+ WHERE usage_date = ?
+ `).all(todayKey);
+
+ const dailyLimitByProfile = new Map();
+ limitRows.forEach((row) => {
+ const profileNumber = sanitizeProfileNumber(row.profile_number);
+ if (profileNumber) {
+ dailyLimitByProfile.set(profileNumber, normalizeAIAutoCommentDailyLimit(row.daily_limit));
+ }
+ });
+
+ const usageByProfile = new Map();
+ usageRows.forEach((row) => {
+ const profileNumber = sanitizeProfileNumber(row.profile_number);
+ if (profileNumber) {
+ usageByProfile.set(profileNumber, Math.max(0, parseInt(row.used_count, 10) || 0));
+ }
+ });
+
+ return Array.from({ length: MAX_PROFILES }, (_unused, index) => {
+ const profileNumber = index + 1;
+ return buildAIAutoCommentLimitPayload(
+ profileNumber,
+ dailyLimitByProfile.get(profileNumber) || 0,
+ usageByProfile.get(profileNumber) || 0
+ );
+ });
+}
+
+function getAIAutoCommentProfileLimit(profileNumber) {
+ const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
+ if (!normalizedProfileNumber) {
+ return null;
+ }
+
+ const todayKey = getLocalDateKey();
+ const limitRow = db.prepare(`
+ SELECT daily_limit
+ FROM ai_profile_auto_comment_limits
+ WHERE profile_number = ?
+ `).get(normalizedProfileNumber);
+ const usageRow = db.prepare(`
+ SELECT used_count
+ FROM ai_profile_auto_comment_usage
+ WHERE profile_number = ?
+ AND usage_date = ?
+ `).get(normalizedProfileNumber, todayKey);
+
+ return buildAIAutoCommentLimitPayload(
+ normalizedProfileNumber,
+ limitRow ? limitRow.daily_limit : 0,
+ usageRow ? usageRow.used_count : 0
+ );
+}
+
+function saveAIAutoCommentProfileLimits(profileLimits) {
+ const rows = Array.isArray(profileLimits) ? profileLimits : [];
+ const normalizedByProfile = new Map();
+
+ rows.forEach((entry) => {
+ const profileNumber = sanitizeProfileNumber(entry && entry.profile_number);
+ if (!profileNumber) {
+ return;
+ }
+ normalizedByProfile.set(profileNumber, normalizeAIAutoCommentDailyLimit(entry.daily_limit));
+ });
+
+ const upsertLimit = db.prepare(`
+ INSERT INTO ai_profile_auto_comment_limits (profile_number, daily_limit, updated_at)
+ VALUES (@profile_number, @daily_limit, CURRENT_TIMESTAMP)
+ ON CONFLICT(profile_number) DO UPDATE SET
+ daily_limit = excluded.daily_limit,
+ updated_at = CURRENT_TIMESTAMP
+ `);
+
+ const persist = db.transaction(() => {
+ for (let profileNumber = 1; profileNumber <= MAX_PROFILES; profileNumber += 1) {
+ upsertLimit.run({
+ profile_number: profileNumber,
+ daily_limit: normalizedByProfile.has(profileNumber)
+ ? normalizedByProfile.get(profileNumber)
+ : 0
+ });
+ }
+ });
+
+ persist();
+ return listAIAutoCommentProfileLimits();
+}
+
+function incrementAIAutoCommentProfileUsage(profileNumber) {
+ const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
+ if (!normalizedProfileNumber) {
+ return null;
+ }
+
+ const todayKey = getLocalDateKey();
+ db.prepare(`
+ INSERT INTO ai_profile_auto_comment_usage (profile_number, usage_date, used_count, updated_at)
+ VALUES (?, ?, 1, CURRENT_TIMESTAMP)
+ ON CONFLICT(profile_number, usage_date) DO UPDATE SET
+ used_count = ai_profile_auto_comment_usage.used_count + 1,
+ updated_at = CURRENT_TIMESTAMP
+ `).run(normalizedProfileNumber, todayKey);
+
+ return getAIAutoCommentProfileLimit(normalizedProfileNumber);
+}
+
function normalizeCreatorName(value) {
if (typeof value !== 'string') {
return null;
@@ -1580,6 +1743,24 @@ db.exec(`
);
`);
+db.exec(`
+ CREATE TABLE IF NOT EXISTS ai_profile_auto_comment_limits (
+ profile_number INTEGER PRIMARY KEY,
+ daily_limit INTEGER NOT NULL DEFAULT 0,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+`);
+
+db.exec(`
+ CREATE TABLE IF NOT EXISTS ai_profile_auto_comment_usage (
+ profile_number INTEGER NOT NULL,
+ usage_date TEXT NOT NULL,
+ used_count INTEGER NOT NULL DEFAULT 0,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (profile_number, usage_date)
+ );
+`);
+
db.exec(`
CREATE TABLE IF NOT EXISTS search_seen_posts (
url TEXT PRIMARY KEY,
@@ -2049,6 +2230,8 @@ ensureColumn('posts', 'is_successful', 'is_successful INTEGER DEFAULT 0');
ensureColumn('ai_settings', 'active_credential_id', 'active_credential_id INTEGER');
ensureColumn('ai_settings', 'prompt_prefix', 'prompt_prefix TEXT');
ensureColumn('ai_settings', 'enabled', 'enabled INTEGER DEFAULT 0');
+ensureColumn('ai_profile_auto_comment_limits', 'daily_limit', 'daily_limit INTEGER NOT NULL DEFAULT 0');
+ensureColumn('ai_profile_auto_comment_usage', 'used_count', 'used_count INTEGER NOT NULL DEFAULT 0');
ensureColumn('ai_credentials', 'is_active', 'is_active INTEGER DEFAULT 1');
ensureColumn('ai_credentials', 'priority', 'priority INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'base_url', 'base_url TEXT');
@@ -6676,7 +6859,8 @@ app.get('/api/ai-settings', (req, res) => {
res.json({
...settings,
- active_credential: activeCredential
+ active_credential: activeCredential,
+ profile_limits: listAIAutoCommentProfileLimits()
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -6685,7 +6869,7 @@ app.get('/api/ai-settings', (req, res) => {
app.put('/api/ai-settings', (req, res) => {
try {
- const { active_credential_id, prompt_prefix, enabled } = req.body;
+ const { active_credential_id, prompt_prefix, enabled, profile_limits } = req.body;
const existing = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();
@@ -6702,6 +6886,10 @@ app.put('/api/ai-settings', (req, res) => {
`).run(active_credential_id || null, prompt_prefix, enabled ? 1 : 0);
}
+ const savedProfileLimits = Array.isArray(profile_limits)
+ ? saveAIAutoCommentProfileLimits(profile_limits)
+ : listAIAutoCommentProfileLimits();
+
const updated = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();
let activeCredential = null;
@@ -6711,7 +6899,8 @@ app.put('/api/ai-settings', (req, res) => {
res.json({
...updated,
- active_credential: activeCredential
+ active_credential: activeCredential,
+ profile_limits: savedProfileLimits
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -7317,6 +7506,8 @@ app.post('/api/ai/generate-comment', async (req, res) => {
try {
const { postText, profileNumber, preferredCredentialId } = requestBody;
+ const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
+ let profileLimitInfo = null;
if (!postText) {
return respondWithTrackedError(400, 'postText is required');
@@ -7348,6 +7539,29 @@ app.post('/api/ai/generate-comment', async (req, res) => {
return respondWithTrackedError(400, 'No active AI credentials available');
}
+ if (traceSource === AI_AUTO_COMMENT_SOURCE) {
+ if (!normalizedProfileNumber) {
+ return respondWithTrackedError(400, 'Für die AI-Kommentar-Aktion ist ein gültiges Profil erforderlich');
+ }
+
+ const limitCheckStartedMs = timingStart();
+ const currentProfileLimit = getAIAutoCommentProfileLimit(normalizedProfileNumber);
+ if (currentProfileLimit && currentProfileLimit.blocked) {
+ timingEnd('profileLimitCheckMs', limitCheckStartedMs);
+ return respondWithTrackedError(
+ 429,
+ `Tageslimit für ${currentProfileLimit.profile_name} erreicht (${currentProfileLimit.used_today}/${currentProfileLimit.daily_limit}). Die Aktion ist bis morgen gesperrt.`,
+ {
+ responseMeta: {
+ profileLimit: currentProfileLimit
+ }
+ }
+ );
+ }
+ profileLimitInfo = incrementAIAutoCommentProfileUsage(normalizedProfileNumber);
+ timingEnd('profileLimitCheckMs', limitCheckStartedMs);
+ }
+
let orderedCredentials = credentials;
if (typeof preferredCredentialId !== 'undefined' && preferredCredentialId !== null) {
const parsedPreferredId = Number(preferredCredentialId);
@@ -7362,7 +7576,6 @@ app.post('/api/ai/generate-comment', async (req, res) => {
const promptBuildStartedMs = timingStart();
let promptPrefix = settings.prompt_prefix || '';
- const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (normalizedProfileNumber) {
const friends = db.prepare('SELECT friend_names FROM profile_friends WHERE profile_number = ?').get(normalizedProfileNumber);
@@ -7433,7 +7646,8 @@ app.post('/api/ai/generate-comment', async (req, res) => {
responseMeta: {
usedCredential: credential.name,
usedCredentialId: credential.id,
- attempts: attemptDetails
+ attempts: attemptDetails,
+ profileLimit: profileLimitInfo
},
totalDurationMs: backendTimings.totalMs
});
@@ -7447,6 +7661,7 @@ app.post('/api/ai/generate-comment', async (req, res) => {
usedCredentialId: credential.id,
attempts: attemptDetails,
rateLimitInfo: rateInfo || null,
+ profileLimitInfo,
traceId,
flowId,
timings: {
diff --git a/web/index.html b/web/index.html
index bd0a434..9ec0365 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1145,6 +1145,14 @@
+
+