openvibers/openvibe.live Commits

Chat parity follow-ups on Live (WS-I task 6): bans end at expires_at read as a UTC instant (datetime()), not text, so a timeout given as ISO no longer lasts until midnight UTC on Live's other surfaces; the floating chat widget applies the deleted_ids Chat returns with cursor reads, so what was deleted while it was away disappears

OP OpenVibers committed 26 Sept 2026 · 0df5662584 · 4 files, +41 −5


Changes

ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 82543a62..caf242a6 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -2364,7 +2364,11 @@ async function hydrateWidgetOnly({ reason = 'open' } = {}) {
if (_fcwLastId) {
const d = await api(`/chat/global/history?after_id=${_fcwLastId}&limit=200`);
if (!d || d.complete === false) await fresh();
-- else if (d.messages && d.messages.length) { feed(d.messages); _fcwCacheAppend(d.messages); }
++ else {
++ if (d.messages && d.messages.length) { feed(d.messages); _fcwCacheAppend(d.messages); }
++ // What was deleted while the widget was away (Chat returns deleted_ids with cursor reads).
++ if (Array.isArray(d.deleted_ids) && d.deleted_ids.length) removeMessagesFromDom(d.deleted_ids);
++ }
} else {
await fresh();
}
ddiff --git a/server/chat/live-context-routes.js b/server/chat/live-context-routes.js
iindex bf831f21..422e8e4e 100644
---- a/server/chat/live-context-routes.js
++++ b/server/chat/live-context-routes.js
@@@ -190,7 +190,7 @@ contextRouter.get('/bans', (req, res) => {
if (req.query.version && String(req.query.version) === version) return res.json({ version, unchanged: true });
res.json({
version,
-- bans: db.all('SELECT id, stream_id, user_id, ip_address, anon_id, expires_at FROM bans WHERE expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP'),
++ bans: db.all('SELECT id, stream_id, user_id, ip_address, anon_id, expires_at FROM bans WHERE expires_at IS NULL OR datetime(expires_at) > CURRENT_TIMESTAMP'),
});
});
ddiff --git a/server/db/database.js b/server/db/database.js
iindex ce4e8261..671965e7 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -5546,7 +5546,7 @@ function isUserBanned(userId, streamId) {
SELECT * FROM bans
WHERE user_id = ?
AND (stream_id = ? OR stream_id IS NULL)
-- AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
++ AND (expires_at IS NULL OR datetime(expires_at) > CURRENT_TIMESTAMP)
LIMIT 1
`, [userId, streamId]);
return !!ban;
@@@ -5566,7 +5566,7 @@ function _normalizeBanIp(ip) {
}
function _cidrBanList() {
if (_cidrBans.list && Date.now() - _cidrBans.at < 15000) return _cidrBans.list;
-- const rows = all(`SELECT * FROM bans WHERE ip_address LIKE '%/%' AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)`);
++ const rows = all(`SELECT * FROM bans WHERE ip_address LIKE '%/%' AND (expires_at IS NULL OR datetime(expires_at) > CURRENT_TIMESTAMP)`);
const list = [];
for (const r of rows) {
const [addr, bitsStr] = String(r.ip_address).split('/');
@@@ -5589,7 +5589,7 @@ function getIpBan(ip, streamId) {
SELECT * FROM bans
WHERE ip_address IN (?, ?)
AND (stream_id = ? OR stream_id IS NULL)
-- AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
++ AND (expires_at IS NULL OR datetime(expires_at) > CURRENT_TIMESTAMP)
LIMIT 1
`, [String(ip), norm, streamId]);
if (ban) return ban;
ddiff --git a/test/ban-expiry.test.js b/test/ban-expiry.test.js
nnew file mode 100644
iindex 00000000..c7051750
---- /dev/null
++++ b/test/ban-expiry.test.js
@@@ -0,0 +1,32 @@
++'use strict';
++// A ban ends at expires_at read as a UTC instant, not as text (roadmap WS-I task 6, found by Chat's
++// parity script): /timeout stores ISO ('…T…Z'), which as TEXT sorts after the same day's
++// 'YYYY-MM-DD HH:MM:SS', so a 60-second timeout blocked the user on Live until midnight UTC.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++
++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-live-banexp-'));
++process.env.DB_PATH = path.join(tmp, 'live.db');
++process.env.NODE_ENV = 'test';
++const log = console.log; console.log = () => {};
++const db = require('../server/db/database');
++db.initDb();
++console.log = log;
++const d = db.getDb();
++d.prepare("INSERT INTO users (id, username, email, password_hash) VALUES (1, 'ann', 'a@x', 'x'), (2, 'bob', 'b@x', 'x'), (3, 'cat', 'c@x', 'x')").run();
++const iso = (ms) => new Date(Date.now() + ms).toISOString();
++const sqlite = (ms) => new Date(Date.now() + ms).toISOString().replace('T', ' ').slice(0, 19);
++// ann: an ISO timeout that ended a minute ago (the bug kept it until midnight UTC)
++d.prepare('INSERT INTO bans (user_id, stream_id, reason, expires_at) VALUES (1, NULL, ?, ?)').run('timeout', iso(-60000));
++// bob: an ISO timeout that ends in a minute; cat: SQLite-format, ended an hour ago
++d.prepare('INSERT INTO bans (user_id, stream_id, reason, expires_at) VALUES (2, NULL, ?, ?)').run('timeout', iso(60000));
++d.prepare('INSERT INTO bans (user_id, stream_id, reason, expires_at) VALUES (3, NULL, ?, ?)').run('timeout', sqlite(-3600000));
++assert.strictEqual(db.isUserBanned(1, null), false, 'an ISO timeout that ended is over');
++assert.strictEqual(db.isUserBanned(2, null), true, 'an ISO timeout still running holds');
++assert.strictEqual(db.isUserBanned(3, null), false, 'a SQLite-format expiry in the past is over');
++d.prepare('INSERT INTO bans (user_id, stream_id, reason, expires_at) VALUES (3, NULL, ?, NULL)').run('ban');
++assert.strictEqual(db.isUserBanned(3, null), true, 'a ban without an end holds');
++fs.rmSync(tmp, { recursive: true, force: true });
++console.log('ban expiry: all checks passed');