openvibers/openvibe.quest Commits

Remove account merge event handling (#266)

OP OpenVibers committed 2 days ago · 3668ca072e · 5 files, +3 −151

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex 228fab6..1dddcf1 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -34,7 +34,6 @@
"notYet": [
"Community-made quests (streamers, communities, mods): they need moderation, which this release does not have",
"Repeatable quests and seasonal resets: every quest completes once per person",
-- "Account merges (ADR-029): quest_progress, quest_completions and quest_badges rows are not re-keyed when a Network account is merged into another (deletion is answered, ADR-033; a merge's aliases are erased with the account)",
"Quest chains, leaderboards and per-community quests; a streamer's getting-started steps still live on OpenVibe.Live"
],
"capabilitiesRegistered": true,
ddiff --git a/server/events-consumer.js b/server/events-consumer.js
iindex 247933a..987f6b9 100644
---- a/server/events-consumer.js
++++ b/server/events-consumer.js
@@@ -36,12 +36,11 @@ const { parseDelivery, createPgInbox } = require('openvibe-sdk/events');
const catalog = require('./quests/catalog');
const { apply } = require('./quests/apply');
const accountDataLib = require('./identity/account-data');
--const subjectMerge = require('./identity/subject-merge');
const CONSUMER = 'quest';
--// The catalog's events plus the account topics (ADR-033) and account merges (ADR-029): one subscription list, so boot
++// The catalog's events plus the account topics (ADR-033): one subscription list, so boot
// subscribes to all of them.
--const TOPICS = Object.freeze([...catalog.EVENT_TYPES, ...accountDataLib.TOPICS, subjectMerge.TOPIC]);
++const TOPICS = Object.freeze([...catalog.EVENT_TYPES, ...accountDataLib.TOPICS]);
const EVENT_ID_RE = /^evt_[0-9A-HJKMNP-TV-Z]{26}$/;
function createEventsConsumer({ db, s, secrets = [], coins = null, items = null, accountData = null, accountSend = null, appQuests = null, daily = null, now = () => Date.now(), log = console }) {
@@@ -81,21 +80,6 @@ function createEventsConsumer({ db, s, secrets = [], coins = null, items = null,
return res.json({ event_id: event.event_id, outcome: accountOutcome });
}
-- // An account merge (ADR-029) folds one person's rows into the survivor's, once per merge_id
-- // (server/identity/subject-merge.js keeps its own ledger, so it never touches quest_event_inbox either).
-- if (event.event_type === subjectMerge.TOPIC) {
-- let mergeOutcome;
-- try {
-- mergeOutcome = await subjectMerge.apply(event, { s, log });
-- } catch (err) {
-- stats.failed++;
-- log.error(`[Events consumer] ${event.event_id} (${event.event_type}) failed:`, err.message);
-- return problem(503, 'quest.event_failed', 'processing failed; it will be retried');
-- }
-- if (String(mergeOutcome).startsWith('ignored:')) stats.ignored++;
-- else stats.applied++;
-- return res.json({ event_id: event.event_id, outcome: mergeOutcome });
-- }
let outcome;
try {
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex bdf3fa8..27e72ea 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -583,7 +583,7 @@ streamers do not write quests: a quest sends people to do something.</li>
<li>Deleting your OpenVibe account deletes your progress, completions and badges here, and an account export includes
them (ADR-033).</li>
<li>When two of your OpenVibe accounts are merged, everything here moves to the account you keep: each step keeps the
--higher count, a quest both finished counts once, and each badge keeps its first award (ADR-029).</li>
++higher count, a quest both finished counts once, and each badge keeps its first award.</li>
</ul>
<p><a href="/quests">The quests</a> · <a href="/updates">What shipped</a></p>`,
}));
ddiff --git a/server/identity/subject-merge.js b/server/identity/subject-merge.js
ddeleted file mode 100644
iindex 27c2293..0000000
---- a/server/identity/subject-merge.js
++++ /dev/null
@@@ -1,62 +0,0 @@
--'use strict';
--
--/**
-- * network.subject.merged → Quest (ADR-029). Two Network accounts became one: `from` is now an alias of `into`, and
-- * every quest row of `from` becomes `into`'s, once per merge_id (quest_subject_merges):
-- *
-- * quest_progress each step keeps the higher count of the two (counts are capped at the step's target)
-- * quest_completions a quest only `from` finished moves to `into` with its OpenCoins state (a credit still owed is
-- * settled to the survivor); one both finished keeps the survivor's, since each wallet was credited
-- * once and Network merges the wallets itself
-- * quest_badges the earliest award of each badge is kept
-- *
-- * Delivered to POST /internal/events beside the account topics; it never passes through quest_event_inbox.
-- */
--const TOPIC = 'network.subject.merged';
--const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
--const MERGE_RE = /^mrg_[0-9A-HJKMNP-TV-Z]{26}$/;
--
--/** One envelope → 'merged' | 'unchanged' | 'ignored:<why>'. */
--async function apply(ev, { s, log = console }) {
-- if (!ev || ev.event_type !== TOPIC) return 'ignored:type';
-- if (ev.source !== 'network') return 'ignored:source';
-- const p = ev.payload && typeof ev.payload === 'object' ? ev.payload : {};
-- const mergeId = String(p.merge_id || ''), from = String(p.from || ''), into = String(p.into || '');
-- if (!MERGE_RE.test(mergeId) || !SUBJECT_RE.test(from) || !SUBJECT_RE.test(into) || from === into) return 'ignored:payload';
-- const r = await s.tx(async () => {
-- const db = s.db;
-- if (await db.maybe('SELECT 1 FROM quest_subject_merges WHERE merge_id = $1', [mergeId])) return null;
-- const at = s.iso();
-- const progress = Number(await db.value(`WITH moved AS (
-- INSERT INTO quest_progress (subject, quest_id, step, count, updated_at)
-- SELECT $2, quest_id, step, count, updated_at FROM quest_progress WHERE subject = $1
-- ON CONFLICT (subject, quest_id, step) DO UPDATE SET count = GREATEST(quest_progress.count, excluded.count),
-- updated_at = GREATEST(quest_progress.updated_at, excluded.updated_at)
-- RETURNING 1) SELECT count(*)::int FROM moved`, [from, into]));
-- await db.exec('DELETE FROM quest_progress WHERE subject = $1', [from]);
-- const completions = Number(await db.value(`WITH moved AS (
-- UPDATE quest_completions SET subject = $2 WHERE subject = $1
-- AND quest_id NOT IN (SELECT quest_id FROM quest_completions WHERE subject = $2)
-- RETURNING 1) SELECT count(*)::int FROM moved`, [from, into]));
-- await db.exec('DELETE FROM quest_completions WHERE subject = $1', [from]);
-- const badges = Number(await db.value(`WITH moved AS (
-- INSERT INTO quest_badges (subject, badge_id, quest_id, awarded_at)
-- SELECT $2, badge_id, quest_id, awarded_at FROM quest_badges WHERE subject = $1
-- ON CONFLICT (subject, badge_id) DO UPDATE SET awarded_at = LEAST(quest_badges.awarded_at, excluded.awarded_at)
-- RETURNING 1) SELECT count(*)::int FROM moved`, [from, into]));
-- await db.exec('DELETE FROM quest_badges WHERE subject = $1', [from]);
-- await db.exec('INSERT INTO daily_preferences (subject,time_zone) SELECT $2,time_zone FROM daily_preferences WHERE subject=$1 ON CONFLICT (subject) DO NOTHING', [from, into]);
-- await db.exec('DELETE FROM daily_preferences WHERE subject=$1', [from]);
-- await db.exec('UPDATE daily_events SET subject=$2 WHERE subject=$1', [from, into]);
-- await db.exec('INSERT INTO daily_completions (subject,day,completed_at) SELECT $2,day,completed_at FROM daily_completions WHERE subject=$1 ON CONFLICT (subject,day) DO UPDATE SET completed_at=LEAST(daily_completions.completed_at,excluded.completed_at)', [from, into]);
-- await db.exec('DELETE FROM daily_completions WHERE subject=$1', [from]);
-- await db.exec('INSERT INTO quest_subject_merges (merge_id, from_subject, into_subject, progress, completions, badges, merged_at) VALUES ($1, $2, $3, $4, $5, $6, $7)',
-- [mergeId, from, into, progress, completions, badges, at]);
-- return { progress, completions, badges };
-- });
-- if (!r) return 'unchanged';
-- log.log(`[Merge] ${mergeId}: ${r.progress} progress step(s), ${r.completions} completion(s) and ${r.badges} badge(s) now the survivor's`);
-- return 'merged';
--}
--
--module.exports = { apply, TOPIC };
ddiff --git a/test/subject-merge.test.js b/test/subject-merge.test.js
ddeleted file mode 100644
iindex bdaf561..0000000
---- a/test/subject-merge.test.js
++++ /dev/null
@@@ -1,69 +0,0 @@
--'use strict';
--/**
-- * ADR-029: network.subject.merged folds one account's quest rows into the survivor's, through the service's own
-- * /internal/events route. Progress keeps the higher count per step, a quest both finished counts once (the
-- * survivor's), a quest only the folded account finished moves with its coins, each badge keeps its first award, a
-- * redelivery moves nothing twice, and the event never touches quest_event_inbox.
-- */
--const assert = require('assert');
--const { boot, check, done } = require('./helpers/boot');
--const { signDeliveryHeaders } = require('openvibe-sdk/events');
--const questStore = require('../server/quests/store');
--
--const KEEP = 'usr_01JZ0000000000000000000KEP';
--const GONE = 'usr_01JZ0000000000000000000GNE';
--const OTHER = 'usr_01JZ0000000000000000000OTH';
--const SECRET = `whsec_${'fixture'.repeat(6)}`;
--const merged = { event_id: 'evt_01JZ0000000000000000000M01', event_type: 'network.subject.merged', source: 'network', payload: { merge_id: 'mrg_01JZ0000000000000000000MRG', from: GONE, into: KEEP } };
--const bodyOf = (ev) => JSON.stringify({ event: ev, seq: 1 });
--
--(async () => {
-- await check('a merge moves the folded account\'s quests to the survivor once', async () => {
-- const t = await boot({ env: { QUEST_EVENTS_SECRET: SECRET } });
-- try {
-- const s = t.ctx.s;
-- const early = '2026-10-01T00:00:00.000Z', late = '2026-10-10T00:00:00.000Z';
-- // Both started "go-live": the folded account is further along.
-- await questStore.bumpProgress(s.db, { subject: KEEP, questId: 'go-live', step: 0, target: 3, at: late });
-- await questStore.bumpProgress(s.db, { subject: GONE, questId: 'go-live', step: 0, target: 3, at: early });
-- await questStore.bumpProgress(s.db, { subject: GONE, questId: 'go-live', step: 0, target: 3, at: early });
-- // Both finished "follow-someone"; only the folded account finished "first-paste", with coins still owed.
-- await questStore.complete(s.db, { id: s.newId('qcp'), subject: KEEP, questId: 'follow-someone', at: late, coins: 10, coinsState: 'credited' });
-- await questStore.complete(s.db, { id: s.newId('qcp'), subject: GONE, questId: 'follow-someone', at: early, coins: 10, coinsState: 'credited' });
-- await questStore.complete(s.db, { id: s.newId('qcp'), subject: GONE, questId: 'first-paste', at: early, coins: 5, coinsState: 'pending' });
-- await questStore.awardBadge(s.db, { subject: KEEP, badgeId: 'first-follow', questId: 'follow-someone', at: late });
-- await questStore.awardBadge(s.db, { subject: GONE, badgeId: 'first-follow', questId: 'follow-someone', at: early });
-- await questStore.awardBadge(s.db, { subject: GONE, badgeId: 'first-paste', questId: 'first-paste', at: early });
-- await questStore.awardBadge(s.db, { subject: OTHER, badgeId: 'first-paste', questId: 'first-paste', at: early });
--
-- const send = async () => await t.get('/internal/events', { method: 'POST', body: bodyOf(merged), headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(bodyOf(merged), SECRET) } });
-- const res = await send();
-- assert.strictEqual(res.status, 200);
-- assert.strictEqual(res.json().outcome, 'merged');
--
-- const count = await s.db.value("SELECT count FROM quest_progress WHERE subject = $1 AND quest_id = 'go-live' AND step = 0", [KEEP]);
-- assert.strictEqual(Number(count), 2, 'the step keeps the higher count');
-- const comps = await s.db.many('SELECT quest_id, completed_at, coins_state FROM quest_completions WHERE subject = $1 ORDER BY quest_id', [KEEP]);
-- assert.deepStrictEqual(comps.map((c) => [c.quest_id, c.coins_state]), [['first-paste', 'pending'], ['follow-someone', 'credited']], 'one completion per quest; an owed credit moves to the survivor');
-- assert.strictEqual(comps.find((c) => c.quest_id === 'follow-someone').completed_at, late, "a quest both finished keeps the survivor's");
-- const badges = await s.db.many('SELECT badge_id, awarded_at FROM quest_badges WHERE subject = $1 ORDER BY badge_id', [KEEP]);
-- assert.deepStrictEqual(badges.map((b) => [b.badge_id, b.awarded_at]), [['first-follow', early], ['first-paste', early]], 'each badge keeps its first award');
-- for (const table of ['quest_progress', 'quest_completions', 'quest_badges']) {
-- assert.strictEqual(Number(await s.db.value(`SELECT count(*)::int FROM ${table} WHERE subject = $1`, [GONE])), 0, `nothing is left under the folded account in ${table}`);
-- }
-- assert.strictEqual(Number(await s.db.value('SELECT count(*)::int FROM quest_badges WHERE subject = $1', [OTHER])), 1, 'someone else is untouched');
--
-- const again = await send();
-- assert.strictEqual(again.json().outcome, 'unchanged', 'a redelivery moves nothing twice');
-- assert.strictEqual(Number(await s.db.value('SELECT count(*)::int FROM quest_event_inbox WHERE event_id = $1', [merged.event_id])), 0, 'the merge is not claimed by quest_event_inbox');
-- const bad = { ...merged, event_id: 'evt_01JZ0000000000000000000M02', payload: { ...merged.payload, into: GONE } };
-- assert.strictEqual((await t.get('/internal/events', { method: 'POST', body: bodyOf(bad), headers: { 'Content-Type': 'application/json', ...signDeliveryHeaders(bodyOf(bad), SECRET) } })).json().outcome, 'ignored:payload', 'an account merged into itself is ignored');
-- } finally { await t.close(); }
-- });
--
-- await check('boot subscribes to account merges', async () => {
-- assert.ok(require('../server/events-consumer').TOPICS.includes('network.subject.merged'));
-- });
--
-- done();
--})();