diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index c8f0661..29bcdc0 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -95,6 +95,11 @@ async function channelToKState(channel, guild) { type: "m.room_membership", room_id: spaceID }] + }, + "m.room.power_levels/": { + events: { + "m.room.avatar": 0 + } } } @@ -114,24 +119,56 @@ async function createRoom(channel, guild, spaceID, kstate) { if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id const invite = threadParent ? [] : ["@cadence:cadence.moe"] // TODO - const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) - const roomID = await api.createRoom({ - name: convertedName, - topic: convertedTopic, - preset: "private_chat", - visibility: "private", - invite, - initial_state: ks.kstateToState(kstate) + const roomID = await postApplyPowerLevels(kstate, async kstate => { + const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) + const roomID = await api.createRoom({ + name: convertedName, + topic: convertedTopic, + preset: "private_chat", + visibility: "private", + invite, + initial_state: ks.kstateToState(kstate) + }) + + db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) + + return roomID }) - db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) - - // Put the newly created child into the space + // Put the newly created child into the space, no need to await this _syncSpaceMember(channel, spaceID, roomID) return roomID } +/** + * Handling power levels separately. The spec doesn't specify what happens, Dendrite differs, + * and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates. + * We don't want the `events` key to be overridden completely. + * https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210 + * https://github.com/matrix-org/matrix-spec/issues/492 + * @param {any} kstate + * @param {(_: any) => Promise} callback must return room ID + * @returns {Promise} room ID + */ +async function postApplyPowerLevels(kstate, callback) { + const powerLevelContent = kstate["m.room.power_levels/"] + const kstateWithoutPowerLevels = {...kstate} + delete kstateWithoutPowerLevels["m.room.power_levels/"] + + /** @type {string} */ + const roomID = await callback(kstateWithoutPowerLevels) + + // Now *really* apply the power level overrides on top of what Synapse *really* set + if (powerLevelContent) { + const newRoomKState = await roomToKState(roomID) + const newRoomPowerLevelsDiff = ks.diffKState(newRoomKState, {"m.room.power_levels/": powerLevelContent}) + await applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff) + } + + return roomID +} + /** * @param {DiscordTypes.APIGuildChannel} channel */ @@ -290,5 +327,6 @@ module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState module.exports.roomToKState = roomToKState module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom +module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 34aa25f..46fa71f 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -21,23 +21,24 @@ const ks = sync.require("../../matrix/kstate") async function createSpace(guild, kstate) { const name = kstate["m.room.name/"].name const topic = kstate["m.room.topic/"]?.topic || undefined - assert(name) - const roomID = await api.createRoom({ - name, - preset: "private_chat", // cannot join space unless invited - visibility: "private", - power_level_content_override: { - events_default: 100, // space can only be managed by bridge - invite: 0 // any existing member can invite others - }, - invite: ["@cadence:cadence.moe"], // TODO - topic, - creation_content: { - type: "m.space" - }, - initial_state: ks.kstateToState(kstate) + const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => { + return api.createRoom({ + name, + preset: "private_chat", // cannot join space unless invited + visibility: "private", + power_level_content_override: { + events_default: 100, // space can only be managed by bridge + invite: 0 // any existing member can invite others + }, + invite: ["@cadence:cadence.moe"], // TODO + topic, + creation_content: { + type: "m.space" + }, + initial_state: ks.kstateToState(kstate) + }) }) db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID) return roomID diff --git a/db/data-for-test.sql b/db/data-for-test.sql index 4a406c9..ec9f9ec 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS "channel_room" ( "name" TEXT, "nick" TEXT, "thread_parent" TEXT, + "custom_avatar" TEXT, PRIMARY KEY("channel_id") ); CREATE TABLE IF NOT EXISTS "event_message" ( @@ -55,11 +56,11 @@ BEGIN TRANSACTION; INSERT INTO guild_space (guild_id, space_id) VALUES ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe'); -INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES -('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL), -('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL), -('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL), -('1100319550446252084', '!PnyBKvUBOhjuCucEfk:cadence.moe', 'worm-farm', NULL, NULL); +INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES +('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL), +('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL, NULL), +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL), +('1100319550446252084', '!PnyBKvUBOhjuCucEfk:cadence.moe', 'worm-farm', NULL, NULL, NULL); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), diff --git a/matrix/api.js b/matrix/api.js index 9eff6c7..2e0763e 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -169,7 +169,7 @@ async function profileSetAvatarUrl(mxid, avatar_url) { async function setUserPower(roomID, mxid, power) { assert(roomID[0] === "!") assert(mxid[0] === "@") - // Yes it's this hard https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 + // Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "") const users = powerLevels.users || {} if (power != null) { diff --git a/matrix/kstate.js b/matrix/kstate.js index 1b2ca14..e840254 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -1,6 +1,7 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict +const mixin = require("mixin-deep") /** Mutates the input. */ function kstateStripConditionals(kstate) { @@ -43,18 +44,32 @@ function diffKState(actual, target) { // go through each key that it should have for (const key of Object.keys(target)) { if (!key.includes("/")) throw new Error(`target kstate's key "${key}" does not contain a slash separator; if a blank state_key was intended, add a trailing slash to the kstate key.`) - if (key in actual) { + + if (key === "m.room.power_levels/") { + // Special handling for power levels, we want to deep merge the actual and target into the final state. + if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`) + const temp = mixin({}, actual[key], target[key]) + try { + assert.deepEqual(actual[key], temp) + } catch (e) { + // they differ. use the newly prepared object as the diff. + diff[key] = temp + } + + } else if (key in actual) { // diff try { assert.deepEqual(actual[key], target[key]) } catch (e) { - // they differ. reassign the target + // they differ. use the target as the diff. 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 diff --git a/matrix/kstate.test.js b/matrix/kstate.test.js index 1541898..11d5131 100644 --- a/matrix/kstate.test.js +++ b/matrix/kstate.test.js @@ -92,3 +92,57 @@ test("diffKState: detects new properties", t => { } ) }) + +test("diffKState: power levels are mixed together", t => { + const original = { + "m.room.power_levels/": { + "ban": 50, + "events": { + "m.room.name": 100, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": { + "room": 20 + }, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + } + } + const result = diffKState(original, { + "m.room.power_levels/": { + "events": { + "m.room.avatar": 0 + } + } + }) + t.deepEqual(result, { + "m.room.power_levels/": { + "ban": 50, + "events": { + "m.room.name": 100, + "m.room.power_levels": 100, + "m.room.avatar": 0 + }, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": { + "room": 20 + }, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + } + }) + t.notDeepEqual(original, result) +}) diff --git a/package-lock.json b/package-lock.json index 875e329..e808e1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", - "mixin-deep": "^2.0.1", + "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", @@ -2111,9 +2111,9 @@ } }, "node_modules/mixin-deep": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", - "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==", + "version": "3.0.0", + "resolved": "git+ssh://git@github.com/cloudrac3r/mixin-deep.git#2dd70d6b8644263f7ed2c1620506c9eb3f11d32a", + "license": "MIT", "engines": { "node": ">=6" } diff --git a/package.json b/package.json index bc0a0db..67aeade 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", - "mixin-deep": "^2.0.1", + "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", diff --git a/stdin.js b/stdin.js index 61a2a08..ce612f5 100644 --- a/stdin.js +++ b/stdin.js @@ -14,6 +14,7 @@ const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const sendEvent = sync.require("./m2d/actions/send-event") const eventDispatcher = sync.require("./d2m/event-dispatcher") +const ks = sync.require("./matrix/kstate") const guildID = "112760669178241024" const extraContext = {} diff --git a/test/data.js b/test/data.js index 30d108a..6ed2f42 100644 --- a/test/data.js +++ b/test/data.js @@ -27,7 +27,7 @@ module.exports = { "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility: "invited"}, "m.space.parent/!jjWAGMeQdNrVZSSfvz:cadence.moe": { - via: ["cadence.moe"], // TODO: put the proper server here + via: ["cadence.moe"], canonical: true }, "m.room.join_rules/": { @@ -40,6 +40,11 @@ module.exports = { "m.room.avatar/": { discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" + }, + "m.room.power_levels/": { + events: { + "m.room.avatar": 0 + } } } },