From 10a3185823e2c320b864decffdb7634b5de8769e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 22 Jun 2025 22:35:33 +1200 Subject: [PATCH] Give sims enough power to send to read-only rooms --- src/d2m/actions/create-room.js | 5 +- src/d2m/actions/register-user.js | 12 ++++- src/d2m/actions/register-user.test.js | 66 +++++++++++++++++++++++++-- test/data.js | 34 ++++++++++++-- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 4c02fd2..ff5782d 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -40,6 +40,8 @@ const PRIVACY_ENUMS = { const DEFAULT_PRIVACY_LEVEL = 0 +const READ_ONLY_ROOM_EVENTS_DEFAULT_POWER = 50 + /** @type {Map>} channel ID -> Promise */ const inflightRoomCreate = new Map() @@ -146,7 +148,7 @@ async function channelToKState(channel, guild, di) { "m.room.join_rules/": join_rules, /** @type {Ty.Event.M_Power_Levels} */ "m.room.power_levels/": { - events_default: everyoneCanSend ? 0 : 50, + events_default: everyoneCanSend ? 0 : READ_ONLY_ROOM_EVENTS_DEFAULT_POWER, events: { "m.reaction": 0, "m.room.redaction": 0 // only affects redactions of own events, required to be able to un-react @@ -557,6 +559,7 @@ async function createAllForGuild(guildID) { } module.exports.DEFAULT_PRIVACY_LEVEL = DEFAULT_PRIVACY_LEVEL +module.exports.READ_ONLY_ROOM_EVENTS_DEFAULT_POWER = READ_ONLY_ROOM_EVENTS_DEFAULT_POWER module.exports.PRIVACY_ENUMS = PRIVACY_ENUMS module.exports.createRoom = createRoom module.exports.ensureRoom = ensureRoom diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index d231e0f..90528ac 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -15,6 +15,8 @@ const file = sync.require("../../matrix/file") const utils = sync.require("../../discord/utils") /** @type {import("../converters/user-to-mxid")} */ const userToMxid = sync.require("../converters/user-to-mxid") +/** @type {import("./create-room")} */ +const createRoom = sync.require("./create-room") /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore let hasher = null // @ts-ignore @@ -139,6 +141,7 @@ 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) /* * PL 100 = Administrator = People who can brick the room. RATIONALE: * - Administrator. @@ -158,8 +161,14 @@ function memberToPowerLevel(user, member, guild, channel) { * - Moderate Members. */ if (utils.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) + if (!everyoneCanSend && userCanSend) return createRoom.READ_ONLY_ROOM_EVENTS_DEFAULT_POWER /* PL 20 = Mention Everyone for technical reasons. */ - if (utils.hasSomePermissions(permissions, ["MentionEveryone"])) return 20 + const everyoneCanMentionEveryone = utils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) + const userCanMentionEveryone = utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.MentionEveryone) + if (!everyoneCanMentionEveryone && userCanMentionEveryone) return 20 return 0 } @@ -250,3 +259,4 @@ module.exports.ensureSim = ensureSim module.exports.ensureSimJoined = ensureSimJoined module.exports.syncUser = syncUser module.exports.syncAllUsersInRoom = syncAllUsersInRoom +module.exports._memberToPowerLevel = memberToPowerLevel diff --git a/src/d2m/actions/register-user.test.js b/src/d2m/actions/register-user.test.js index 353c89f..f1934cf 100644 --- a/src/d2m/actions/register-user.test.js +++ b/src/d2m/actions/register-user.test.js @@ -1,10 +1,12 @@ -const {_memberToStateContent} = require("./register-user") +const {_memberToStateContent, _memberToPowerLevel} = require("./register-user") const {test} = require("supertape") -const testData = require("../../../test/data") +const data = require("../../../test/data") +const mixin = require("@cloudrac3r/mixin-deep") +const DiscordTypes = require("discord-api-types/v10") test("member2state: without member nick or avatar", async t => { t.deepEqual( - await _memberToStateContent(testData.member.kumaccino.user, testData.member.kumaccino, testData.guild.general.id), + await _memberToStateContent(data.member.kumaccino.user, data.member.kumaccino, data.guild.general.id), { avatar_url: "mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL", displayname: "kumaccino", @@ -24,7 +26,7 @@ test("member2state: without member nick or avatar", async t => { test("member2state: with global name, without member nick or avatar", async t => { t.deepEqual( - await _memberToStateContent(testData.member.papiophidian.user, testData.member.papiophidian, testData.guild.general.id), + await _memberToStateContent(data.member.papiophidian.user, data.member.papiophidian, data.guild.general.id), { avatar_url: "mxc://cadence.moe/JPzSmALLirnIprlSMKohSSoX", displayname: "PapiOphidian", @@ -44,7 +46,7 @@ test("member2state: with global name, without member nick or avatar", async t => test("member2state: with member nick and avatar", async t => { t.deepEqual( - await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), + await _memberToStateContent(data.member.sheep.user, data.member.sheep, data.guild.general.id), { avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", displayname: "The Expert's Submarine", @@ -61,3 +63,57 @@ test("member2state: with member nick and avatar", async t => { } ) }) + +test("member2power: default to zero if member roles unknown", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, null, data.guild.data_horde, data.channel.saving_the_world) + t.equal(power, 0) +}) + +test("member2power: unremarkable = 0", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, { + roles: [] + }, data.guild.data_horde, data.channel.general) + t.equal(power, 0) +}) + +test("member2power: can mention everyone = 20", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, { + roles: ["684524730274807911"] + }, data.guild.data_horde, data.channel.general) + t.equal(power, 20) +}) + +test("member2power: can send messages in protected channel due to role = 50", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, { + roles: ["684524730274807911"] + }, data.guild.data_horde, data.channel.saving_the_world) + t.equal(power, 50) +}) + +test("member2power: can send messages in protected channel due to user override = 50", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, { + roles: [] + }, data.guild.data_horde, mixin({}, data.channel.saving_the_world, { + permission_overwrites: data.channel.saving_the_world.permission_overwrites.concat({ + type: DiscordTypes.OverwriteType.member, + id: data.user.clyde_ai.id, + allow: String(DiscordTypes.PermissionFlagsBits.SendMessages), + deny: "0" + }) + })) + t.equal(power, 50) +}) + +test("member2power: can kick users = 50", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, { + roles: ["682789592390281245"] + }, data.guild.data_horde, data.channel.general) + t.equal(power, 50) +}) + +test("member2power: can manage channels = 100", async t => { + const power = _memberToPowerLevel(data.user.clyde_ai, { + roles: ["665290147377578005"] + }, data.guild.data_horde, data.channel.saving_the_world) + t.equal(power, 100) +}) diff --git a/test/data.js b/test/data.js index fba0587..aba31d3 100644 --- a/test/data.js +++ b/test/data.js @@ -37,18 +37,31 @@ module.exports = { id: "1161864271370666075", guild_id: "112760669178241024" }, + /** @type {DiscordTypes.APITextChannel} */ saving_the_world: { type: 0, topic: "Anything and everything archiving/preservation related", rate_limit_per_user: 0, position: 0, - permission_overwrites: [], + permission_overwrites: [ + { + id: "665289423482519565", + type: DiscordTypes.OverwriteType.Role, + allow: "0", + deny: String(DiscordTypes.PermissionFlagsBits.SendMessages) + }, + { + id: "684524730274807911", + type: DiscordTypes.OverwriteType.Role, + allow: String(DiscordTypes.PermissionFlagsBits.SendMessages), + deny: "0" + } + ], parent_id: null, name: "saving-the-world", last_pin_timestamp: "2021-04-14T18:39:41+00:00", last_message_id: "1335828749479837750", id: "665310973967597573", - flags: 0, guild_id: "665289423482519565" } }, @@ -349,7 +362,7 @@ module.exports = { unicode_emoji: null, tags: {}, position: 0, - permissions: "2221982107557441", + permissions: "968619318849", name: "@everyone", mentionable: false, managed: false, @@ -374,6 +387,21 @@ module.exports = { flags: 0, color: 1752220 }, + { + version: 1683791258594, + unicode_emoji: null, + tags: {}, + position: 22, + permissions: "8194", + name: "Moderator", + mentionable: true, + managed: false, + id: "682789592390281245", + icon: null, + hoist: false, + flags: 0, + color: 1752220 + }, { version: 1683791258580, unicode_emoji: null,