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
Changes
@@@ -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();}@@@ -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'),});});@@@ -5546,7 +5546,7 @@ function isUserBanned(userId, streamId) {SELECT * FROM bansWHERE 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 bansWHERE 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;nnew file mode 100644@@@ -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');
@@ -2364,7 +2364,11 @@ async function hydrateWidgetOnly({ reason = 'open' } = {}) {
@@ -2364,7 +2364,11 @@ async function hydrateWidgetOnly({ reason = 'open' } = {}) {
if (_fcwLastId) {
if (_fcwLastId) {
const d = await api(`/chat/global/history?after_id=${_fcwLastId}&limit=200`);
const d = await api(`/chat/global/history?after_id=${_fcwLastId}&limit=200`);
if (!d || d.complete === false) await fresh();
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 {
} else {
await fresh();
await fresh();
}
}
@@ -190,7 +190,7 @@ contextRouter.get('/bans', (req, res) => {
@@ -190,7 +190,7 @@ contextRouter.get('/bans', (req, res) => {
if (req.query.version && String(req.query.version) === version) return res.json({ version, unchanged: true });
if (req.query.version && String(req.query.version) === version) return res.json({ version, unchanged: true });
res.json({
res.json({
version,
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'),
});
});
});
});
@@ -5546,7 +5546,7 @@ function isUserBanned(userId, streamId) {
@@ -5546,7 +5546,7 @@ function isUserBanned(userId, streamId) {
SELECT * FROM bans
SELECT * FROM bans
WHERE user_id = ?
WHERE user_id = ?
AND (stream_id = ? OR stream_id IS NULL)
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
LIMIT 1
`, [userId, streamId]);
`, [userId, streamId]);
return !!ban;
return !!ban;
@@ -5566,7 +5566,7 @@ function _normalizeBanIp(ip) {
@@ -5566,7 +5566,7 @@ function _normalizeBanIp(ip) {
}
}
function _cidrBanList() {
function _cidrBanList() {
if (_cidrBans.list && Date.now() - _cidrBans.at < 15000) return _cidrBans.list;
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 = [];
const list = [];
for (const r of rows) {
for (const r of rows) {
const [addr, bitsStr] = String(r.ip_address).split('/');
const [addr, bitsStr] = String(r.ip_address).split('/');
@@ -5589,7 +5589,7 @@ function getIpBan(ip, streamId) {
@@ -5589,7 +5589,7 @@ function getIpBan(ip, streamId) {
SELECT * FROM bans
SELECT * FROM bans
WHERE ip_address IN (?, ?)
WHERE ip_address IN (?, ?)
AND (stream_id = ? OR stream_id IS NULL)
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
LIMIT 1
`, [String(ip), norm, streamId]);
`, [String(ip), norm, streamId]);
if (ban) return ban;
if (ban) return ban;
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -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');