openvibers/openvibe.network Commits

Go-live announcements: persisted per-streamer cooldown (60m) + daily cap (8) gating inbox/push/email/Discord

OP OpenVibers committed 27 Aug 2026 · f930ff7291 · 3 files, +35 −1


Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex d5f667ce..9700231b 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -523,6 +523,8 @@ function initDb(dbPath) {
seed.run('email_daily_cap', '2000', 'number');
seed.run('email_verify_user_daily_cap', '6', 'number');
seed.run('resend_webhook_secret', '', 'string');
++ seed.run('stream_live_cooldown_min', '60', 'number');
++ seed.run('stream_live_daily_cap', '8', 'number');
} catch (e) { console.warn('[DB] email verification migration:', e.message); }
// ── Migration: one linked account per (user, service) ────────
ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex 85bcface..6586cd40 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -227,6 +227,38 @@ router.post('/events/stream-live', async (req, res) => {
const results = { discord: null, notifications: null };
++ // ── Per-streamer rate limit (persisted — survives deploys) ───────────────────
++ // A streamer flapping off/on (reconnects, restarts, OBS hiccups) must not re-announce
++ // every time. One fan-out per streamer per `stream_live_cooldown_min` (default 60) and
++ // at most `stream_live_daily_cap` (default 8) per rolling 24h — gating inbox, push,
++ // email AND Discord. `force:true` (admin/manual) bypasses the cooldown, not the cap.
++ {
++ const db = getDb(req);
++ try {
++ db.exec(`CREATE TABLE IF NOT EXISTS stream_live_announcements (
++ id INTEGER PRIMARY KEY AUTOINCREMENT, streamer_key TEXT NOT NULL, stream_id TEXT, sent_at DATETIME DEFAULT CURRENT_TIMESTAMP);
++ CREATE INDEX IF NOT EXISTS idx_sla_key ON stream_live_announcements(streamer_key, sent_at DESC)`);
++ } catch { /* */ }
++ const key = String(streamer.network_id || streamer.id || streamer.username).toLowerCase();
++ const cooldownMin = Math.max(1, parseInt(db.getSetting('stream_live_cooldown_min'), 10) || 60);
++ const dailyCap = Math.max(1, parseInt(db.getSetting('stream_live_daily_cap'), 10) || 8);
++ const last = db.prepare('SELECT sent_at FROM stream_live_announcements WHERE streamer_key = ? ORDER BY sent_at DESC LIMIT 1').get(key);
++ const today = db.prepare("SELECT COUNT(*) AS c FROM stream_live_announcements WHERE streamer_key = ? AND sent_at > datetime('now','-1 day')").get(key)?.c || 0;
++ const lastMs = last ? new Date(String(last.sent_at).replace(' ', 'T') + 'Z').getTime() : 0;
++ const sinceMin = last ? (Date.now() - lastMs) / 60000 : Infinity;
++ if (today >= dailyCap) {
++ console.log(`[StreamLive] ${streamer.username}: daily cap (${dailyCap}) reached — not announcing`);
++ return res.json({ ok: true, skipped: true, reason: 'daily-cap', announced_today: today, cap: dailyCap });
++ }
++ if (!req.body.force && sinceMin < cooldownMin) {
++ const nextAt = new Date(lastMs + cooldownMin * 60000).toISOString();
++ console.log(`[StreamLive] ${streamer.username}: announced ${Math.round(sinceMin)}m ago — cooling down until ${nextAt}`);
++ return res.json({ ok: true, skipped: true, reason: 'cooldown', next_allowed_at: nextAt });
++ }
++ db.prepare('INSERT INTO stream_live_announcements (streamer_key, stream_id) VALUES (?, ?)').run(key, stream.id != null ? String(stream.id) : null);
++ db.prepare("DELETE FROM stream_live_announcements WHERE sent_at < datetime('now','-7 days')").run();
++ }
++
// ── Discord Alert ────────────────────────────────────────
const discordService = req.app.locals.discordService;
if (discordService) {
ddiff --git a/server/notifications/notification-service.js b/server/notifications/notification-service.js
iindex cd245aaf..7ce44a55 100644
---- a/server/notifications/notification-service.js
++++ b/server/notifications/notification-service.js
@@@ -91,7 +91,7 @@ class NotificationService {
// already has the alert — reconnects/restarts must not stack ten of them.
this._recentFromSender = db.prepare(`
SELECT id FROM notifications
-- WHERE user_id = ? AND type = ? AND sender_id = ? AND is_dismissed = 0
++ WHERE user_id = ? AND type = ? AND sender_id = ?
AND created_at > datetime('now', '-60 minutes')
LIMIT 1
`);