openvibers/openvibe.chat Commits

Respect consent for personal chat AI profiles (#513)

OP OpenVibers committed 3 days ago · 077956db25 · 3 files, +37 −1

Changes

ddiff --git a/server/ai/chat-ai.js b/server/ai/chat-ai.js
iindex 1745cb1..4339763 100644
---- a/server/ai/chat-ai.js
++++ b/server/ai/chat-ai.js
@@@ -379,6 +379,14 @@ async function _refreshUser(uid, maxId) {
if (!dayRows.length) dayRows = await db.getChatMessagesForAi({ userId: uid, order: 'desc', limit: 80 });
if (!dayRows.length) return false;
++ // A personal profile is still AI training use: respect the same Network consent as the shared
++ // summaries, and erase the cached profile if consent was withdrawn (or cannot be verified).
++ dayRows = await consent.filterRows(dayRows);
++ if (!dayRows.length) {
++ await db.deleteChatAiSubjectMemory(uid);
++ return false;
++ }
++
const uname = dayRows[dayRows.length - 1].username || `user#${uid}`;
const priorMemory = prior ? (prior.memory_json || '') : '';
const totalSeen = (prior ? (prior.message_count || 0) : 0);
@@@ -643,7 +651,7 @@ async function getAnonInsight(anonId) {
}
module.exports = {
-- start, stop, _tick, _msgData, _topics, _people, _refreshRoom, _refreshChannel, WORKFLOW_GLOBAL,
++ start, stop, _tick, _msgData, _topics, _people, _refreshRoom, _refreshChannel, _refreshUser, WORKFLOW_GLOBAL,
getGlobalInsight, getUserInsight, getRelayUserInsight, getAnonInsight,
WORKFLOW_PROFILE,
};
ddiff --git a/server/db/database.js b/server/db/database.js
iindex f2828ab..2cac81d 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -1279,6 +1279,15 @@ async function getChatAiSummaries(scope, subjectId) {
return await all('SELECT * FROM chat_ai_summaries WHERE scope = ? AND subject_id = ? ORDER BY "window"',
[scope, subjectId || 0]);
}
++async function deleteChatAiSubjectMemory(subjectId) {
++ const id = Number(subjectId);
++ if (!Number.isSafeInteger(id) || id <= 0) return false;
++ return await transaction(async () => {
++ await run("DELETE FROM chat_ai_summaries WHERE scope = 'user' AND subject_id = ?", [id]);
++ await run("DELETE FROM chat_timeline_events WHERE scope = 'user' AND subject_id = ?", [id]);
++ return true;
++ });
++}
// Paginated + searchable timeline browse. `before` = epoch ms (exclusive upper bound); `q`
// filters label/detail; `since` = epoch ms lower bound (for period jumps). Newest first.
async function getChatTimelineEvents({ scope = 'global', subjectId = 0, before = null, since = null, q = null, limit = 25 } = {}) {
@@@ -1569,6 +1578,7 @@ module.exports = {
// chat AI (Live's server/ai/chat-ai.js, moved): the job's selection/read helpers and the routes' reads
getChatAiSummary,
getChatAiSummaries,
++ deleteChatAiSubjectMemory,
getChatTimelineEvents,
getMaxChatMessageId,
countChatMessagesSince,
ddiff --git a/test/chat-ai.test.js b/test/chat-ai.test.js
iindex 7e2e279..3e981d6 100644
---- a/test/chat-ai.test.js
++++ b/test/chat-ai.test.js
@@@ -221,6 +221,24 @@ t('a consent check that fails leaves everyone out (fail closed)', async () => {
consent._reset();
});
++t('a personal summary is erased when Network consent is absent', async () => {
++ const consent = require('../server/net/consent');
++ consent._reset();
++ await msg({ user_id: alice.id, username: 'alice', message: 'a private topic' });
++ await db.upsertChatAiSummary({
++ scope: 'user', subject_id: alice.id, window: 'rolling', overview: 'cached',
++ memory_json: 'cached memory', timeline_json: '[{"ts":"2026-01-01 00:00:00","label":"cached"}]',
++ });
++ await db.addChatTimelineEvents('user', alice.id, [{ ts: '2026-01-01 00:00:00', label: 'cached' }]);
++ h.consent.allowed.delete(alice.subject_id);
++
++ await chatAi._refreshUser(alice.id, 1);
++
++ assert.strictEqual(await db.getChatAiSummary('user', alice.id, 'rolling'), null);
++ const events = await db.getChatTimelineEvents({ scope: 'user', subjectId: alice.id, limit: 10 });
++ assert.deepStrictEqual(events, []);
++});
++
t('GET /api/v1/rooms/global/insight answers the room shape, publicly', async () => {
await db.upsertChatAiSummary({
scope: 'global', subject_id: 0, window: 'global', overview: 'arrivals see this', memory_json: 'm',