From f418d51e555ee80681932e6b4f51b2cf618ec5e6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 10 May 2023 17:40:31 +1200 Subject: [PATCH] refactor kstate, add stub user syncing function --- d2m/actions/create-room.js | 76 +++----------------------- d2m/actions/create-room.test.js | 76 +------------------------- d2m/actions/register-user.js | 10 ++++ matrix/kstate.js | 65 +++++++++++++++++++++++ matrix/kstate.test.js | 94 +++++++++++++++++++++++++++++++++ test/test.js | 3 +- 6 files changed, 181 insertions(+), 143 deletions(-) create mode 100644 matrix/kstate.js create mode 100644 matrix/kstate.test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index b254951..7f2c799 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -9,50 +9,15 @@ const { discord, sync, db } = passthrough const file = sync.require("../../matrix/file") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") - -function kstateStripConditionals(kstate) { - for (const [k, content] of Object.entries(kstate)) { - if ("$if" in content) { - if (content.$if) delete content.$if - else delete kstate[k] - } - } - return kstate -} - -function kstateToState(kstate) { - const events = [] - for (const [k, content] of Object.entries(kstate)) { - // conditional for whether a key is even part of the kstate (doing this declaratively on json is hard, so represent it as a property instead.) - if ("$if" in content && !content.$if) continue - delete content.$if - - const [type, state_key] = k.split("/") - assert.ok(typeof type === "string") - assert.ok(typeof state_key === "string") - events.push({type, state_key, content}) - } - return events -} - -/** - * @param {import("../../types").Event.BaseStateEvent[]} events - * @returns {any} - */ -function stateToKState(events) { - const kstate = {} - for (const event of events) { - kstate[event.type + "/" + event.state_key] = event.content - } - return kstate -} +/** @type {import("../../matrix/kstate")} */ +const ks = sync.require("../../matrix/kstate") /** * @param {string} roomID */ async function roomToKState(roomID) { const root = await api.getAllState(roomID) - return stateToKState(root) + return ks.stateToKState(root) } /** @@ -60,33 +25,12 @@ async function roomToKState(roomID) { * @params {any} kstate */ function applyKStateDiffToRoom(roomID, kstate) { - const events = kstateToState(kstate) + const events = ks.kstateToState(kstate) return Promise.all(events.map(({type, state_key, content}) => api.sendState(roomID, type, state_key, content) )) } -function diffKState(actual, target) { - const diff = {} - // go through each key that it should have - for (const key of Object.keys(target)) { - if (key in actual) { - // diff - try { - assert.deepEqual(actual[key], target[key]) - } catch (e) { - // they differ. reassign the target - diff[key] = target[key] - } - } else { - // not present, needs to be added - diff[key] = target[key] - } - // keys that are missing in "actual" will not be deleted on "target" (no action) - } - return diff -} - /** * @param {import("discord-api-types/v10").APIGuildTextChannel} channel * @param {import("discord-api-types/v10").APIGuild} guild @@ -98,7 +42,7 @@ async function channelToKState(channel, guild) { const avatarEventContent = {} if (guild.icon) { avatarEventContent.discord_path = file.guildIcon(guild) - avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) + avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API } const channelKState = { @@ -138,7 +82,7 @@ async function createRoom(channel, guild, spaceID, kstate) { preset: "private_chat", visibility: "private", invite: ["@cadence:cadence.moe"], // TODO - initial_state: kstateToState(kstate) + initial_state: ks.kstateToState(kstate) }) db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, roomID) @@ -204,12 +148,12 @@ async function _syncRoom(channelID, shouldActuallySync) { // sync channel state to room const roomKState = await roomToKState(existing) - const roomDiff = diffKState(roomKState, channelKState) + const roomDiff = ks.diffKState(roomKState, channelKState) const roomApply = applyKStateDiffToRoom(existing, roomDiff) // sync room as space member const spaceKState = await roomToKState(spaceID) - const spaceDiff = diffKState(spaceKState, { + const spaceDiff = ks.diffKState(spaceKState, { [`m.space.child/${existing}`]: { via: ["cadence.moe"] // TODO: use the proper server } @@ -241,8 +185,4 @@ module.exports.createRoom = createRoom module.exports.ensureRoom = ensureRoom module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild -module.exports.kstateToState = kstateToState -module.exports.stateToKState = stateToKState -module.exports.diffKState = diffKState module.exports.channelToKState = channelToKState -module.exports.kstateStripConditionals = kstateStripConditionals diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index 5ce52e8..ab390fc 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -1,80 +1,8 @@ -const {kstateToState, stateToKState, diffKState, channelToKState, kstateStripConditionals} = require("./create-room") +const {channelToKState} = require("./create-room") +const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") const testData = require("../../test/data") -test("kstate2state: general", t => { - t.deepEqual(kstateToState({ - "m.room.name/": {name: "test name"}, - "m.room.member/@cadence:cadence.moe": {membership: "join"} - }), [ - { - type: "m.room.name", - state_key: "", - content: { - name: "test name" - } - }, - { - type: "m.room.member", - state_key: "@cadence:cadence.moe", - content: { - membership: "join" - } - } - ]) -}) - -test("state2kstate: general", t => { - t.deepEqual(stateToKState([ - { - type: "m.room.name", - state_key: "", - content: { - name: "test name" - } - }, - { - type: "m.room.member", - state_key: "@cadence:cadence.moe", - content: { - membership: "join" - } - } - ]), { - "m.room.name/": {name: "test name"}, - "m.room.member/@cadence:cadence.moe": {membership: "join"} - }) -}) - -test("diffKState: detects edits", t => { - t.deepEqual( - diffKState({ - "m.room.name/": {name: "test name"}, - "same/": {a: 2} - }, { - "m.room.name/": {name: "edited name"}, - "same/": {a: 2} - }), - { - "m.room.name/": {name: "edited name"} - } - ) -}) - -test("diffKState: detects new properties", t => { - t.deepEqual( - diffKState({ - "m.room.name/": {name: "test name"}, - }, { - "m.room.name/": {name: "test name"}, - "new/": {a: 2} - }), - { - "new/": {a: 2} - } - ) -}) - test("channel2room: general", async t => { t.deepEqual( kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.channelKState)), diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 04b0998..89bac2c 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -78,5 +78,15 @@ async function ensureSimJoined(user, roomID) { return mxid } +/** + * @param {import("discord-api-types/v10").APIUser} user + * @param {Required>} member + */ +async function memberToStateContent(user, member) { + return { + displayname: member.nick || user.username + } +} + module.exports.ensureSim = ensureSim module.exports.ensureSimJoined = ensureSimJoined diff --git a/matrix/kstate.js b/matrix/kstate.js new file mode 100644 index 0000000..398b1b6 --- /dev/null +++ b/matrix/kstate.js @@ -0,0 +1,65 @@ +// @ts-check + +const assert = require("assert") + +/** Mutates the input. */ +function kstateStripConditionals(kstate) { + for (const [k, content] of Object.entries(kstate)) { + // conditional for whether a key is even part of the kstate (doing this declaratively on json is hard, so represent it as a property instead.) + if ("$if" in content) { + if (content.$if) delete content.$if + else delete kstate[k] + } + } + return kstate +} + +function kstateToState(kstate) { + const events = [] + kstateStripConditionals(kstate) + for (const [k, content] of Object.entries(kstate)) { + const [type, state_key] = k.split("/") + assert.ok(typeof type === "string") + assert.ok(typeof state_key === "string") + events.push({type, state_key, content}) + } + return events +} + +/** + * @param {import("../types").Event.BaseStateEvent[]} events + * @returns {any} + */ +function stateToKState(events) { + const kstate = {} + for (const event of events) { + kstate[event.type + "/" + event.state_key] = event.content + } + return kstate +} + +function diffKState(actual, target) { + const diff = {} + // go through each key that it should have + for (const key of Object.keys(target)) { + if (key in actual) { + // diff + try { + assert.deepEqual(actual[key], target[key]) + } catch (e) { + // they differ. reassign the target + diff[key] = target[key] + } + } else { + // not present, needs to be added + diff[key] = target[key] + } + // keys that are missing in "actual" will not be deleted on "target" (no action) + } + return diff +} + +module.exports.kstateStripConditionals = kstateStripConditionals +module.exports.kstateToState = kstateToState +module.exports.stateToKState = stateToKState +module.exports.diffKState = diffKState diff --git a/matrix/kstate.test.js b/matrix/kstate.test.js new file mode 100644 index 0000000..ed59e9d --- /dev/null +++ b/matrix/kstate.test.js @@ -0,0 +1,94 @@ +const {kstateToState, stateToKState, diffKState, kstateStripConditionals} = require("./kstate") +const {test} = require("supertape") + +test("kstate strip: strips false conditions", t => { + t.deepEqual(kstateStripConditionals({ + a: {$if: false, value: 2}, + b: {value: 4} + }), { + b: {value: 4} + }) +}) + +test("kstate strip: keeps true conditions while removing $if", t => { + t.deepEqual(kstateStripConditionals({ + a: {$if: true, value: 2}, + b: {value: 4} + }), { + a: {value: 2}, + b: {value: 4} + }) +}) + +test("kstate2state: general", t => { + t.deepEqual(kstateToState({ + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }), [ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]) +}) + +test("state2kstate: general", t => { + t.deepEqual(stateToKState([ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]), { + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }) +}) + +test("diffKState: detects edits", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + "same/": {a: 2} + }, { + "m.room.name/": {name: "edited name"}, + "same/": {a: 2} + }), + { + "m.room.name/": {name: "edited name"} + } + ) +}) + +test("diffKState: detects new properties", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + }, { + "m.room.name/": {name: "test name"}, + "new/": {a: 2} + }), + { + "new/": {a: 2} + } + ) +}) diff --git a/test/test.js b/test/test.js index 4e01708..bf0023e 100644 --- a/test/test.js +++ b/test/test.js @@ -12,7 +12,8 @@ const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) +require("../matrix/kstate.test") +require("../matrix/api.test") require("../matrix/read-registration.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") -require("../matrix/api.test")