diff --git a/scripts/migrate-from-old-bridge.js b/scripts/migrate-from-old-bridge.js index 36cf884..40d6993 100755 --- a/scripts/migrate-from-old-bridge.js +++ b/scripts/migrate-from-old-bridge.js @@ -37,7 +37,9 @@ const createRoom = sync.require("../d2m/actions/create-room") /** @type {import("../src/matrix/mreq")} */ const mreq = sync.require("../matrix/mreq") /** @type {import("../src/matrix/api")} */ -const api = sync.require("../matrix/api") +const api = sync.require("../src/matrix/api") +/** @type {import("../src/matrix/utils")} */ +const utils = sync.require("../src/matrix/utils") const sema = new Semaphore() @@ -89,7 +91,7 @@ async function migrateGuild(guild) { throw e } } - await api.setUserPower(roomID, newBridgeMxid, 100) + await utils.setUserPower(roomID, newBridgeMxid, 100, api) }) await api.joinRoom(roomID) diff --git a/scripts/remove-old-bridged-users.js b/scripts/remove-old-bridged-users.js index d8910bd..756a492 100644 --- a/scripts/remove-old-bridged-users.js +++ b/scripts/remove-old-bridged-users.js @@ -10,6 +10,7 @@ const passthrough = require("../src/passthrough") Object.assign(passthrough, {db, sync}) const api = require("../src/matrix/api") +const utils = require("../src/matrix/utils") const mreq = require("../src/matrix/mreq") const rooms = db.prepare("select room_id from channel_room").pluck().all() @@ -25,7 +26,7 @@ const rooms = db.prepare("select room_id from channel_room").pluck().all() await api.leaveRoom(roomID, mxid) } } - await api.setUserPower(roomID, "@_discord_bot:cadence.moe", 0) + await utils.setUserPower(roomID, "@_discord_bot:cadence.moe", 0, api) await api.leaveRoom(roomID) } catch (e) { if (e.message.includes("Appservice not in room")) { diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index c5fdd60..5c2b76c 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -17,8 +17,8 @@ const mreq = sync.require("../../matrix/mreq") const ks = sync.require("../../matrix/kstate") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") -/** @type {import("../../m2d/converters/utils")} */ -const mUtils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const mUtils = sync.require("../../matrix/utils") /** @type {import("./create-space")} */ const createSpace = sync.require("./create-space") diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index 85e8a08..966263b 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -12,7 +12,9 @@ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") /** @type {import("../../discord/utils")} */ -const utils = sync.require("../../discord/utils") +const dUtils = sync.require("../../discord/utils") +/** @type {import("../../matrix/utils")} */ +const mxUtils = sync.require("../../matrix/utils") /** @type {import("../converters/user-to-mxid")} */ const userToMxid = sync.require("../converters/user-to-mxid") /** @type {import("./create-room")} */ @@ -159,8 +161,8 @@ async function memberToStateContent(user, member, guildID) { function memberToPowerLevel(user, member, guild, channel) { if (!member) return 0 - const permissions = utils.getPermissions(member.roles, guild.roles, user.id, channel.permission_overwrites) - const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) + const permissions = dUtils.getPermissions(member.roles, guild.roles, user.id, channel.permission_overwrites) + const everyonePermissions = dUtils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) /* * PL 100 = Administrator = People who can brick the room. RATIONALE: * - Administrator. @@ -169,7 +171,7 @@ function memberToPowerLevel(user, member, guild, channel) { * - Manage Channels: People who can manage the channel can delete it. * (Setting sim users to PL 100 is safe because even though we can't demote the sims we can use code to make the sims demote themselves.) */ - if (guild.owner_id === user.id || utils.hasSomePermissions(permissions, ["Administrator", "ManageWebhooks", "ManageGuild", "ManageChannels"])) return 100 + if (guild.owner_id === user.id || dUtils.hasSomePermissions(permissions, ["Administrator", "ManageWebhooks", "ManageGuild", "ManageChannels"])) return 100 /* * PL 50 = Moderator = People who can manage people and messages in many ways. RATIONALE: * - Manage Messages: Can moderate by pinning or deleting the conversation. @@ -179,14 +181,14 @@ function memberToPowerLevel(user, member, guild, channel) { * - Mute Members & Deafen Members: Can moderate by silencing disruptive people in ways they can't undo. * - Moderate Members. */ - if (utils.hasSomePermissions(permissions, ["ManageMessages", "ManageNicknames", "ManageThreads", "KickMembers", "BanMembers", "MuteMembers", "DeafenMembers", "ModerateMembers"])) return 50 + if (dUtils.hasSomePermissions(permissions, ["ManageMessages", "ManageNicknames", "ManageThreads", "KickMembers", "BanMembers", "MuteMembers", "DeafenMembers", "ModerateMembers"])) return 50 /* PL 50 = if room is read-only but the user has been specially allowed to send messages */ - const everyoneCanSend = utils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages) - const userCanSend = utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.SendMessages) + const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages) + const userCanSend = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.SendMessages) if (!everyoneCanSend && userCanSend) return createRoom.READ_ONLY_ROOM_EVENTS_DEFAULT_POWER /* PL 20 = Mention Everyone for technical reasons. */ - const everyoneCanMentionEveryone = utils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) - const userCanMentionEveryone = utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) + const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) + const userCanMentionEveryone = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) if (!everyoneCanMentionEveryone && userCanMentionEveryone) return 20 return 0 } @@ -247,7 +249,7 @@ async function _sendSyncUser(roomID, mxid, content, powerLevel, options) { actions.push(api.sendState(roomID, "m.room.member", mxid, content, mxid)) // Update power levels if (powerLevel != null) { - actions.push(api.setUserPower(roomID, mxid, powerLevel)) + actions.push(mxUtils.setUserPower(roomID, mxid, powerLevel, api)) } // Update global profile (if supported by server) if (await supportsMsc4069) { diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index 18adc16..8e4c9a2 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -6,8 +6,8 @@ const passthrough = require("../../passthrough") const {sync, select, from} = passthrough /** @type {import("./message-to-event")} */ const messageToEvent = sync.require("../converters/message-to-event") -/** @type {import("../../m2d/converters/utils")} */ -const utils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") function eventCanBeEdited(ev) { // Discord does not allow files, images, attachments, or videos to be edited. diff --git a/src/d2m/converters/message-to-event.embeds.test.js b/src/d2m/converters/message-to-event.embeds.test.js index cddd427..85a08cc 100644 --- a/src/d2m/converters/message-to-event.embeds.test.js +++ b/src/d2m/converters/message-to-event.embeds.test.js @@ -1,7 +1,7 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const data = require("../../../test/data") -const {mockGetEffectivePower} = require("../../m2d/converters/utils.test") +const {mockGetEffectivePower} = require("../../matrix/utils.test") const {db} = require("../../passthrough") test("message2event embeds: nothing but a field", async t => { diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 5360702..85ee969 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -14,8 +14,8 @@ const file = sync.require("../../matrix/file") const emojiToKey = sync.require("./emoji-to-key") /** @type {import("../actions/lottie")} */ const lottie = sync.require("../actions/lottie") -/** @type {import("../../m2d/converters/utils")} */ -const mxUtils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const mxUtils = sync.require("../../matrix/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") const {reg} = require("../../matrix/read-registration") diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 4b213e4..05ec5be 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -2,7 +2,7 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const {MatrixServerError} = require("../../matrix/mreq") const data = require("../../../test/data") -const {mockGetEffectivePower} = require("../../m2d/converters/utils.test") +const {mockGetEffectivePower} = require("../../matrix/utils.test") const Ty = require("../../types") /** diff --git a/src/d2m/converters/remove-reaction.js b/src/d2m/converters/remove-reaction.js index caa96d1..4ca22b6 100644 --- a/src/d2m/converters/remove-reaction.js +++ b/src/d2m/converters/remove-reaction.js @@ -5,8 +5,8 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const {discord, sync, select} = passthrough -/** @type {import("../../m2d/converters/utils")} */ -const utils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** * @typedef ReactionRemoveRequest diff --git a/src/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js index 98b8f12..575b3c5 100644 --- a/src/d2m/converters/thread-to-announcement.js +++ b/src/d2m/converters/thread-to-announcement.js @@ -4,8 +4,8 @@ const assert = require("assert").strict const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough -/** @type {import("../../m2d/converters/utils")} */ -const mxUtils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const mxUtils = sync.require("../../matrix/utils") const {reg} = require("../../matrix/read-registration.js") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) diff --git a/src/d2m/converters/thread-to-announcement.test.js b/src/d2m/converters/thread-to-announcement.test.js index 8d011fd..3286f62 100644 --- a/src/d2m/converters/thread-to-announcement.test.js +++ b/src/d2m/converters/thread-to-announcement.test.js @@ -2,7 +2,7 @@ const {test} = require("supertape") const {threadToAnnouncement} = require("./thread-to-announcement") const data = require("../../../test/data") const Ty = require("../../types") -const {mockGetEffectivePower} = require("../../m2d/converters/utils.test") +const {mockGetEffectivePower} = require("../../matrix/utils.test") /** * @param {string} roomID diff --git a/src/discord/interactions/permissions.js b/src/discord/interactions/permissions.js index c780a2a..036947f 100644 --- a/src/discord/interactions/permissions.js +++ b/src/discord/interactions/permissions.js @@ -9,8 +9,8 @@ const {InteractionMethods} = require("snowtransfer") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @type {import("../../m2d/converters/utils")} */ -const utils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** * @param {DiscordTypes.APIContextMenuGuildInteraction} interaction @@ -126,7 +126,7 @@ async function* _interactEdit({data, guild_id, message}, {api}) { assert(spaceID) // Do it - await api.setUserPowerCascade(spaceID, mxid, power) + await utils.setUserPowerCascade(spaceID, mxid, power, api) // ACK yield {editOriginalInteractionResponse: { diff --git a/src/discord/interactions/permissions.test.js b/src/discord/interactions/permissions.test.js index ef3fef2..5a078b5 100644 --- a/src/discord/interactions/permissions.test.js +++ b/src/discord/interactions/permissions.test.js @@ -2,7 +2,7 @@ const {test} = require("supertape") const DiscordTypes = require("discord-api-types/v10") const {select, db} = require("../../passthrough") const {_interact, _interactEdit} = require("./permissions") -const {mockGetEffectivePower} = require("../../m2d/converters/utils.test") +const {mockGetEffectivePower} = require("../../matrix/utils.test") /** * @template T @@ -156,7 +156,7 @@ test("permissions: reports permissions of selected matrix user (admin v11 cannot }) test("permissions: can update user to moderator", async t => { - let called = 0 + let called = [] const msgs = await fromAsync(_interactEdit({ data: { target_id: "1128118177155526666", @@ -168,22 +168,48 @@ test("permissions: can update user to moderator", async t => { guild_id: "112760669178241024" }, { api: { - async setUserPowerCascade(roomID, mxid, power) { - called++ - t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe") // space ID - t.equal(mxid, "@cadence:cadence.moe") - t.equal(power, 50) + async getStateEvent(roomID, type, key) { + called.push("get power levels") + t.equal(type, "m.room.power_levels") + return {} + }, + async getStateEventOuter(roomID, type, key) { + called.push("get room create") + return { + type: "m.room.create", + state_key: "", + sender: "@_ooye_bot:cadence.moe", + event_id: "$create", + origin_server_ts: 0, + room_id: roomID, + content: { + room_version: "11" + } + } + }, + async *generateFullHierarchy(spaceID) { + called.push("generate full hierarchy") + }, + async sendState(roomID, type, key, content) { + called.push("set power levels") + t.ok(["!hierarchy", "!jjmvBegULiLucuWEHU:cadence.moe"].includes(roomID), `expected room ID to be in hierarchy, but was ${roomID}`) + t.equal(type, "m.room.power_levels") + t.equal(key, "") + t.deepEqual(content, { + users: {"@cadence:cadence.moe": 50} + }) + return "$updated" } } })) t.equal(msgs.length, 2) t.equal(msgs[0].createInteractionResponse.data.content, "Updating `@cadence:cadence.moe` to **moderator**, please wait...") t.equal(msgs[1].editOriginalInteractionResponse.content, "Updated `@cadence:cadence.moe` to **moderator**.") - t.equal(called, 1) + t.deepEqual(called, ["generate full hierarchy", "get room create", "get power levels", "set power levels"]) }) test("permissions: can update user to default", async t => { - let called = 0 + let called = [] const msgs = await fromAsync(_interactEdit({ data: { target_id: "1128118177155526666", @@ -195,16 +221,44 @@ test("permissions: can update user to default", async t => { guild_id: "112760669178241024" }, { api: { - async setUserPowerCascade(roomID, mxid, power) { - called++ - t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe") // space ID - t.equal(mxid, "@cadence:cadence.moe") - t.equal(power, 0) + async getStateEvent(roomID, type, key) { + called.push("get power levels") + t.equal(type, "m.room.power_levels") + return { + users: {"@cadence:cadence.moe": 50} + } + }, + async getStateEventOuter(roomID, type, key) { + called.push("get room create") + return { + type: "m.room.create", + state_key: "", + sender: "@_ooye_bot:cadence.moe", + event_id: "$create", + origin_server_ts: 0, + room_id: roomID, + content: { + room_version: "11" + } + } + }, + async *generateFullHierarchy(spaceID) { + called.push("generate full hierarchy") + }, + async sendState(roomID, type, key, content) { + called.push("set power levels") + t.ok(["!hierarchy", "!jjmvBegULiLucuWEHU:cadence.moe"].includes(roomID), `expected room ID to be in hierarchy, but was ${roomID}`) + t.equal(type, "m.room.power_levels") + t.equal(key, "") + t.deepEqual(content, { + users: {} + }) + return "$updated" } } })) t.equal(msgs.length, 2) t.equal(msgs[0].createInteractionResponse.data.content, "Updating `@cadence:cadence.moe` to **default**, please wait...") t.equal(msgs[1].editOriginalInteractionResponse.content, "Updated `@cadence:cadence.moe` to **default**.") - t.equal(called, 1) + t.deepEqual(called, ["generate full hierarchy", "get room create", "get power levels", "set power levels"]) }) diff --git a/src/discord/interactions/reactions.js b/src/discord/interactions/reactions.js index 59bf065..bd2f856 100644 --- a/src/discord/interactions/reactions.js +++ b/src/discord/interactions/reactions.js @@ -7,8 +7,8 @@ const {InteractionMethods} = require("snowtransfer") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @type {import("../../m2d/converters/utils")} */ -const utils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** * @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction diff --git a/src/m2d/actions/add-reaction.js b/src/m2d/actions/add-reaction.js index 268888c..2b19fb2 100644 --- a/src/m2d/actions/add-reaction.js +++ b/src/m2d/actions/add-reaction.js @@ -5,8 +5,8 @@ const Ty = require("../../types") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough -/** @type {import("../converters/utils")} */ -const utils = sync.require("../converters/utils") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** @type {import("../converters/emoji")} */ const emoji = sync.require("../converters/emoji") diff --git a/src/m2d/actions/redact.js b/src/m2d/actions/redact.js index 9b26e8e..9f99ec1 100644 --- a/src/m2d/actions/redact.js +++ b/src/m2d/actions/redact.js @@ -5,8 +5,8 @@ const Ty = require("../../types") const passthrough = require("../../passthrough") const {discord, sync, db, select, from} = passthrough -/** @type {import("../converters/utils")} */ -const utils = sync.require("../converters/utils") +/** @type {import("../../matrix/utils")} */ +const utils = sync.require("../../matrix/utils") /** * @param {Ty.Event.Outer_M_Room_Redaction} event diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index a030ac5..785243d 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -14,8 +14,8 @@ const {tag} = require("@cloudrac3r/html-template-tag") const passthrough = require("../../passthrough") const {sync, db, discord, select, from} = passthrough const {reg} = require("../../matrix/read-registration") -/** @type {import("../converters/utils")} */ -const mxUtils = sync.require("../converters/utils") +/** @type {import("../../matrix/utils")} */ +const mxUtils = sync.require("../../matrix/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") /** @type {import("../../matrix/file")} */ diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index b326962..13c0af1 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -20,8 +20,8 @@ const redact = sync.require("./actions/redact") const updatePins = sync.require("./actions/update-pins") /** @type {import("../matrix/matrix-command-handler")} */ const matrixCommandHandler = sync.require("../matrix/matrix-command-handler") -/** @type {import("./converters/utils")} */ -const utils = sync.require("./converters/utils") +/** @type {import("../matrix/utils")} */ +const utils = sync.require("../matrix/utils") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") /** @type {import("../d2m/actions/create-room")} */ diff --git a/src/matrix/api.js b/src/matrix/api.js index 824f13b..01f9c3b 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -358,55 +358,6 @@ async function profileSetAvatarUrl(mxid, avatar_url, inhibitPropagate) { } } -/** - * Set a user's power level within a room. - * @param {string} roomID - * @param {string} mxid - * @param {number} newPower - */ -async function setUserPower(roomID, mxid, newPower) { - assert(roomID[0] === "!") - assert(mxid[0] === "@") - // Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 - const power = await getStateEvent(roomID, "m.room.power_levels", "") - power.users = power.users || {} - - // Check if it has really changed to avoid sending a useless state event - // (Can't diff kstate here because of (a) circular imports (b) kstate has special behaviour diffing power levels) - const oldPowerLevel = power.users?.[mxid] ?? power.users_default ?? 0 - if (oldPowerLevel === newPower) return - - // Bridge bot can't demote equal power users, so need to decide which user will send the event - const botPowerLevel = power.users?.[`@${reg.sender_localpart}:${reg.ooye.server_name}`] ?? power.users_default ?? 0 - const eventSender = oldPowerLevel >= botPowerLevel ? mxid : undefined - - // Update the event content - if (newPower == null || newPower === (power.users_default ?? 0)) { - delete power.users[mxid] - } else { - power.users[mxid] = newPower - } - - await sendState(roomID, "m.room.power_levels", "", power, eventSender) - return power -} - -/** - * Set a user's power level for a whole room hierarchy. - * @param {string} spaceID - * @param {string} mxid - * @param {number} power - */ -async function setUserPowerCascade(spaceID, mxid, power) { - assert(spaceID[0] === "!") - assert(mxid[0] === "@") - const rooms = await getFullHierarchy(spaceID) - await setUserPower(spaceID, mxid, power) - for (const room of rooms) { - await setUserPower(room.room_id, mxid, power) - } -} - async function ping() { // not using mreq so that we can read the status code const res = await fetch(`${mreq.baseUrl}/client/v1/appservice/${reg.id}/ping`, { @@ -579,8 +530,6 @@ module.exports.redactEvent = redactEvent module.exports.sendTyping = sendTyping module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetAvatarUrl = profileSetAvatarUrl -module.exports.setUserPower = setUserPower -module.exports.setUserPowerCascade = setUserPowerCascade module.exports.ping = ping module.exports.getMedia = getMedia module.exports.sendReadReceipt = sendReadReceipt diff --git a/src/matrix/kstate.js b/src/matrix/kstate.js index ace9c36..85a2838 100644 --- a/src/matrix/kstate.js +++ b/src/matrix/kstate.js @@ -10,8 +10,8 @@ const {sync} = passthrough const file = sync.require("./file") /** @type {import("./api")} */ const api = sync.require("./api") -/** @type {import("../m2d/converters/utils")} */ -const utils = sync.require("../m2d/converters/utils") +/** @type {import("./utils")} */ +const utils = sync.require("./utils") /** Mutates the input. Not recursive - can only include or exclude entire state events. */ function kstateStripConditionals(kstate) { diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 601b2dc..e30ae6f 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -8,8 +8,8 @@ const sharp = require("sharp") const {discord, sync, db, select} = require("../passthrough") /** @type {import("./api")}) */ const api = sync.require("./api") -/** @type {import("../m2d/converters/utils")} */ -const mxUtils = sync.require("../m2d/converters/utils") +/** @type {import("./utils")} */ +const mxUtils = sync.require("./utils") /** @type {import("../discord/utils")} */ const dUtils = sync.require("../discord/utils") /** @type {import("./kstate")} */ diff --git a/src/matrix/room-upgrade.js b/src/matrix/room-upgrade.js index 0b02762..7f0d4e6 100644 --- a/src/matrix/room-upgrade.js +++ b/src/matrix/room-upgrade.js @@ -10,8 +10,8 @@ const {discord, db, sync, as, select, from} = require("../passthrough") const api = sync.require("./api") /** @type {import("../d2m/actions/create-room")}) */ const createRoom = sync.require("../d2m/actions/create-room") -/** @type {import("../m2d/converters/utils")}) */ -const utils = sync.require("../m2d/converters/utils") +/** @type {import("./utils")}) */ +const utils = sync.require("./utils") const roomUpgradeSema = new Semaphore() diff --git a/src/m2d/converters/utils.js b/src/matrix/utils.js similarity index 84% rename from src/m2d/converters/utils.js rename to src/matrix/utils.js index ccdef83..3860f6e 100644 --- a/src/m2d/converters/utils.js +++ b/src/matrix/utils.js @@ -1,11 +1,11 @@ // @ts-check const assert = require("assert").strict -const Ty = require("../../types") -const passthrough = require("../../passthrough") +const Ty = require("../types") +const passthrough = require("../passthrough") const {db} = passthrough -const {reg} = require("../../matrix/read-registration") +const {reg} = require("./read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore @@ -129,7 +129,7 @@ class MatrixStringBuilder { * https://spec.matrix.org/v1.9/appendices/#routing * https://gitdab.com/cadence/out-of-your-element/issues/11 * @param {string} roomID - * @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("../../matrix/api")[K]} | {getEffectivePower: (roomID: string, mxids: string[], api: any) => Promise<{powers: Record, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter, powerLevels: Ty.Event.M_Power_Levels}>, getJoinedMembers: import("../../matrix/api")["getJoinedMembers"]}} api + * @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("./api")[K]} | {getEffectivePower: (roomID: string, mxids: string[], api: any) => Promise<{powers: Record, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter, powerLevels: Ty.Event.M_Power_Levels}>, getJoinedMembers: import("./api")["getJoinedMembers"]}} api */ async function getViaServers(roomID, api) { const candidates = [] @@ -188,7 +188,7 @@ async function getViaServers(roomID, api) { * https://spec.matrix.org/v1.9/appendices/#routing * https://gitdab.com/cadence/out-of-your-element/issues/11 * @param {string} roomID - * @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api + * @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("./api")[K]}} api * @returns {Promise} */ async function getViaServersQuery(roomID, api) { @@ -273,7 +273,7 @@ function removeCreatorsFromPowerLevels(roomCreateOuter, powerLevels) { * @template {string} T * @param {string} roomID * @param {T[]} mxids - * @param {{[K in "getStateEvent" | "getStateEventOuter"]: import("../../matrix/api")[K]}} api + * @param {{[K in "getStateEvent" | "getStateEventOuter"]: import("./api")[K]}} api * @returns {Promise<{powers: Record, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter, powerLevels: Ty.Event.M_Power_Levels}>} */ async function getEffectivePower(roomID, mxids, api) { @@ -300,6 +300,56 @@ async function getEffectivePower(roomID, mxids, api) { return {powers, allCreators, tombstone, roomCreate, powerLevels} } +/** + * Set a user's power level within a room. + * @param {string} roomID + * @param {string} mxid + * @param {number} newPower + * @param {{[K in "getStateEvent" | "getStateEventOuter" | "sendState"]: import("./api")[K]}} api + */ +async function setUserPower(roomID, mxid, newPower, api) { + assert(roomID[0] === "!") + assert(mxid[0] === "@") + // Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 + const {powerLevels, powers: {[mxid]: oldPowerLevel, [bot]: botPowerLevel}} = await getEffectivePower(roomID, [mxid, bot], api) + + // Check if it has really changed to avoid sending a useless state event + if (oldPowerLevel === newPower) return + + // Bridge bot can't demote equal power users, so need to decide which user will send the event + const eventSender = oldPowerLevel >= botPowerLevel ? mxid : undefined + + // Update the event content + powerLevels.users ??= {} + if (newPower == null || newPower === (powerLevels.users_default ?? 0)) { + delete powerLevels.users[mxid] + } else { + powerLevels.users[mxid] = newPower + } + + await api.sendState(roomID, "m.room.power_levels", "", powerLevels, eventSender) +} + +/** + * Set a user's power level for a whole room hierarchy. + * @param {string} spaceID + * @param {string} mxid + * @param {number} power + * @param {{[K in "getStateEvent" | "getStateEventOuter" | "sendState" | "generateFullHierarchy"]: import("./api")[K]}} api + */ +async function setUserPowerCascade(spaceID, mxid, power, api) { + assert(spaceID[0] === "!") + assert(mxid[0] === "@") + let seenSpace = false + for await (const room of api.generateFullHierarchy(spaceID)) { + if (room.room_id === spaceID) seenSpace = true + await setUserPower(room.room_id, mxid, power, api) + } + if (!seenSpace) { + await setUserPower(spaceID, mxid, power, api) + } +} + module.exports.bot = bot module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord @@ -311,3 +361,5 @@ module.exports.getViaServersQuery = getViaServersQuery module.exports.roomHasAtLeastVersion = roomHasAtLeastVersion module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels module.exports.getEffectivePower = getEffectivePower +module.exports.setUserPower = setUserPower +module.exports.setUserPowerCascade = setUserPowerCascade diff --git a/src/m2d/converters/utils.test.js b/src/matrix/utils.test.js similarity index 96% rename from src/m2d/converters/utils.test.js rename to src/matrix/utils.test.js index 9c11393..0ecd41e 100644 --- a/src/m2d/converters/utils.test.js +++ b/src/matrix/utils.test.js @@ -1,7 +1,5 @@ // @ts-check -const e = new Error("Custom error") - const {test} = require("supertape") const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion} = require("./utils") const util = require("util") @@ -41,8 +39,14 @@ test("event hash: hash is different for different inputs", t => { }) test("MatrixStringBuilder: add, addLine, add same text", t => { + const e = { + stack: "Error: Custom error\n at ./example.test.js:3:11)", + toString() { + return "Error: Custom error" + } + } const gatewayMessage = {t: "MY_MESSAGE", d: {display: "Custom message data"}} - let stackLines = e.stack?.split("\n") + let stackLines = e.stack.split("\n") const builder = new MatrixStringBuilder() builder.addLine("\u26a0 Bridged event from Discord not delivered", "\u26a0 Bridged event from Discord not delivered") @@ -63,12 +67,12 @@ test("MatrixStringBuilder: add, addLine, add same text", t => { + "\nError: Custom error" + "\nError trace:" + "\nError: Custom error" - + "\n at ./m2d/converters/utils.test.js:3:11)\n", + + "\n at ./example.test.js:3:11)\n", format: "org.matrix.custom.html", formatted_body: "\u26a0 Bridged event from Discord not delivered" + "
Gateway event: MY_MESSAGE" + "
Error: Custom error" - + "
Error trace
Error: Custom error\n    at ./m2d/converters/utils.test.js:3:11)
" + + "
Error trace
Error: Custom error\n    at ./example.test.js:3:11)
" + `
Original payload
{ display: 'Custom message data' }
` }) }) diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index 8c2d99d..6b80e9d 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -18,7 +18,9 @@ const createSpace = sync.require("../../d2m/actions/create-space") /** @type {import("../auth")} */ const auth = require("../auth") /** @type {import("../../discord/utils")} */ -const utils = sync.require("../../discord/utils") +const dUtils = sync.require("../../discord/utils") +/** @type {import("../../matrix/utils")} */ +const mxUtils = sync.require("../../matrix/utils") const {reg} = require("../../matrix/read-registration") const schema = { @@ -102,8 +104,8 @@ function getChannelRoomsLinks(guild, rooms, roles) { let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)) let removedWrongTypeChannels = filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type)) let removedPrivateChannels = filterTo(unlinkedChannels, c => { - const permissions = utils.getPermissions(roles, guild.roles, botID, c["permission_overwrites"]) - return utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) + const permissions = dUtils.getPermissions(roles, guild.roles, botID, c["permission_overwrites"]) + return dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) }) unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) @@ -228,7 +230,7 @@ as.router.post("/api/invite", defineEventHandler(async event => { ( parsedBody.permissions === "admin" ? 100 : parsedBody.permissions === "moderator" ? 50 : 0) - if (powerLevel) await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel) + if (powerLevel) await mxUtils.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel, api) if (parsedBody.guild_id) { setResponseHeader(event, "HX-Refresh", true) diff --git a/src/web/routes/info.js b/src/web/routes/info.js index 9c202fa..0c3e3b1 100644 --- a/src/web/routes/info.js +++ b/src/web/routes/info.js @@ -4,8 +4,8 @@ const {z} = require("zod") const {defineEventHandler, getValidatedQuery, H3Event} = require("h3") const {as, from, sync, select} = require("../../passthrough") -/** @type {import("../../m2d/converters/utils")} */ -const mUtils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")} */ +const mUtils = sync.require("../../matrix/utils") /** * @param {H3Event} event diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 17f27df..5193ded 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -10,8 +10,8 @@ const {discord, db, as, sync, select, from} = require("../../passthrough") const auth = sync.require("../auth") /** @type {import("../../matrix/mreq")} */ const mreq = sync.require("../../matrix/mreq") -/** @type {import("../../m2d/converters/utils")}*/ -const utils = sync.require("../../m2d/converters/utils") +/** @type {import("../../matrix/utils")}*/ +const utils = sync.require("../../matrix/utils") const {reg} = require("../../matrix/read-registration") /** diff --git a/src/web/server.js b/src/web/server.js index 7c8ed3e..3cb3060 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -13,8 +13,8 @@ const reg = require("../matrix/read-registration") const {sync, discord, as, select} = require("../passthrough") /** @type {import("./pug-sync")} */ const pugSync = sync.require("./pug-sync") -/** @type {import("../m2d/converters/utils")} */ -const mUtils = sync.require("../m2d/converters/utils") +/** @type {import("../matrix/utils")} */ +const mUtils = sync.require("../matrix/utils") const {id} = require("../../addbot") // Pug diff --git a/test/test.js b/test/test.js index 2c3902a..6e2c97b 100644 --- a/test/test.js +++ b/test/test.js @@ -147,6 +147,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/matrix/mreq.test") require("../src/matrix/read-registration.test") require("../src/matrix/txnid.test") + require("../src/matrix/utils.test") require("../src/d2m/actions/create-room.test") require("../src/d2m/actions/create-space.test") require("../src/d2m/actions/register-user.test") @@ -164,7 +165,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/m2d/converters/diff-pins.test") require("../src/m2d/converters/event-to-message.test") require("../src/m2d/converters/emoji.test") - require("../src/m2d/converters/utils.test") require("../src/m2d/converters/emoji-sheet.test") require("../src/discord/interactions/invite.test") require("../src/discord/interactions/matrix-info.test")