From 0f1cf7a20c9b22ef5801146e852f8e3b3483701f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 09:13:25 +1300 Subject: [PATCH 1/3] Fix calls to syncUser/registerUser --- d2m/actions/register-user.js | 12 ++++++++---- d2m/actions/send-message.js | 12 +++++++----- d2m/event-dispatcher.js | 2 +- discord/utils.js | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 7e1d9f9..68b1a88 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const reg = require("../../matrix/read-registration") const DiscordTypes = require("discord-api-types/v10") const mixin = require("mixin-deep") @@ -180,11 +180,12 @@ function _hashProfileContent(content, powerLevel) { * 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time * @param {DiscordTypes.APIUser} user * @param {Omit} member - * @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuildChannel} channel + * @param {DiscordTypes.APIGuild} guild + * @param {string} roomID * @returns {Promise} mxid of the updated sim */ -async function syncUser(user, member, guild, channel, roomID) { +async function syncUser(user, member, channel, guild, roomID) { const mxid = await ensureSimJoined(user, roomID) const content = await memberToStateContent(user, member, guild.id) const powerLevel = memberToPowerLevel(user, member, guild, channel) @@ -204,6 +205,9 @@ async function syncUser(user, member, guild, channel, roomID) { return mxid } +/** + * @param {string} roomID + */ async function syncAllUsersInRoom(roomID) { const mxids = select("sim_member", "mxid", {room_id: roomID}).pluck().all() @@ -228,7 +232,7 @@ async function syncAllUsersInRoom(roomID) { assert.ok(user) console.log(`[user sync] to matrix: ${user.username} in ${channel.name}`) - await syncUser(user, member, guild, channel, roomID) + await syncUser(user, member, channel, guild, roomID) } } diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 8c26f07..a1290e0 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -1,6 +1,7 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough @@ -18,17 +19,18 @@ const createRoom = sync.require("../actions/create-room") const dUtils = sync.require("../../discord/utils") /** - * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message - * @param {import("discord-api-types/v10").APIGuild} guild + * @param {DiscordTypes.GatewayMessageCreateDispatchData} message + * @param {DiscordTypes.APIGuildChannel} channel + * @param {DiscordTypes.APIGuild} guild * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel */ -async function sendMessage(message, guild, row) { +async function sendMessage(message, channel, guild, row) { const roomID = await createRoom.ensureRoom(message.channel_id) let senderMxid = null if (!dUtils.isWebhookMessage(message)) { if (message.member) { // available on a gateway message create event - senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) + senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID) } else { // well, good enough... senderMxid = await registerUser.ensureSimJoined(message.author, roomID) } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 3db1d45..2c4dcd5 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -248,7 +248,7 @@ module.exports = { if (affected) return // @ts-ignore - await sendMessage.sendMessage(message, guild, row), + await sendMessage.sendMessage(message, channel, guild, row), await discordCommandHandler.execute(message, channel, guild) }, diff --git a/discord/utils.js b/discord/utils.js index ff1f215..6b6b602 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -63,7 +63,7 @@ function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { */ function hasPermission(resolvedPermissions, permissionToCheckFor) { // Make sure permissionToCheckFor has exactly one permission in it - assert.equal(permissionToCheckFor.toString(2).match(/1/g), 1) + assert.equal(permissionToCheckFor.toString(2).match(/1/g)?.length, 1) // Do the actual calculation return (resolvedPermissions & permissionToCheckFor) === permissionToCheckFor } From 12d85c982e20adba182f73b7576a02d62b0b9ca4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 10:17:39 +1300 Subject: [PATCH 2/3] Allow Matrixers to @room if Discorders can too --- d2m/actions/create-room.js | 7 ++++++- d2m/actions/create-room.test.js | 11 +++++++++++ discord/utils.js | 2 +- test/data.js | 22 +++++++++++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 0dea283..a8d2846 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -12,6 +12,8 @@ const file = sync.require("../../matrix/file") const api = sync.require("../../matrix/api") /** @type {import("../../matrix/kstate")} */ const ks = sync.require("../../matrix/kstate") +/** @type {import("../../discord/utils")} */ +const utils = sync.require("../../discord/utils") /** @type {import("./create-space")}) */ const createSpace = sync.require("./create-space") // watch out for the require loop @@ -119,6 +121,9 @@ async function channelToKState(channel, guild) { join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]} } + const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites) + const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) + const channelKState = { "m.room.name/": {name: convertedName}, "m.room.topic/": {topic: convertedTopic}, @@ -136,7 +141,7 @@ async function channelToKState(channel, guild) { "m.room.avatar": 0 }, notifications: { - room: 20 // TODO: Matrix users should have the same abilities as unprivileged Discord members. So make this automatically configured based on the guild or channel's default mention everyone permissions. That way if unprivileged Discord members can mention everyone, Matrix users can too. + room: everyoneCanMentionEveryone ? 0 : 20 }, users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {}) }, diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index 93f9203..be0febb 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -1,5 +1,6 @@ // @ts-check +const mixin = require("mixin-deep") const {channelToKState, _convertNameAndTopic} = require("./create-room") const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") @@ -39,6 +40,16 @@ test("channel2room: invite-only privacy room", async t => { ) }) +test("channel2room: room where limited people can mention everyone", async t => { + const limitedGuild = mixin({}, testData.guild.general) + limitedGuild.roles[0].permissions = (BigInt(limitedGuild.roles[0].permissions) - 131072n).toString() + const limitedRoom = mixin({}, testData.room.general, {"m.room.power_levels/": {notifications: {room: 20}}}) + t.deepEqual( + kstateStripConditionals(await channelToKState(testData.channel.general, limitedGuild).then(x => x.channelKState)), + limitedRoom + ) +}) + test("convertNameAndTopic: custom name and topic", t => { t.deepEqual( _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"), diff --git a/discord/utils.js b/discord/utils.js index 6b6b602..2726a1a 100644 --- a/discord/utils.js +++ b/discord/utils.js @@ -26,7 +26,7 @@ function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { } if (channelOverwrites) { - /** @type {((overwrite: Required["permission_overwrites"][0]) => any)[]} */ + /** @type {((overwrite: Required) => any)[]} */ const actions = [ // Channel @everyone deny overwrite => overwrite.id === everyoneID && (allowed &= ~BigInt(overwrite.deny)), diff --git a/test/data.js b/test/data.js index 61ee5d0..c165322 100644 --- a/test/data.js +++ b/test/data.js @@ -47,6 +47,9 @@ module.exports = { }, users: { "@test_auto_invite:example.org": 100 + }, + notifications: { + room: 0 } }, "chat.schildi.hide_ui/read_receipts": {hidden: true}, @@ -98,7 +101,6 @@ module.exports = { icon: "a_f83622e09ead74f0c5c527fe241f8f8c", emojis: [ { - version: 0, roles: [], require_colons: true, name: "hippo", @@ -108,7 +110,6 @@ module.exports = { animated: false }, { - version: 0, roles: [], require_colons: true, name: "hipposcope", @@ -121,7 +122,20 @@ module.exports = { premium_subscription_count: 14, roles: [ { - version: 1696964862461, + unicode_emoji: null, + tags: {}, + position: 0, + permissions: '559623605575360', + name: '@everyone', + mentionable: false, + managed: false, + id: '112760669178241024', + icon: null, + hoist: false, + flags: 0, + color: 0 + }, + { unicode_emoji: null, tags: {}, position: 22, @@ -135,7 +149,6 @@ module.exports = { flags: 0, color: 0 }, { - version: 1696964862776, unicode_emoji: null, tags: {}, position: 131, @@ -149,7 +162,6 @@ module.exports = { flags: 0, color: 11076095 }, { - version: 1696964862698, unicode_emoji: '🍂', tags: {}, position: 102, From a190e690b1b30e9be12f27ab1b1a6a0c52e9df22 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Mar 2024 10:22:49 +1300 Subject: [PATCH 3/3] Add tests for somePermissions/allPermissions check --- discord/utils.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/discord/utils.test.js b/discord/utils.test.js index 1f3783f..815c0a1 100644 --- a/discord/utils.test.js +++ b/discord/utils.test.js @@ -1,3 +1,4 @@ +const DiscordTypes = require("discord-api-types/v10") const {test} = require("supertape") const data = require("../test/data") const utils = require("./utils") @@ -82,3 +83,27 @@ test("getPermissions: channel overwrite to allow role works", t => { const want = BigInt(1 << 10 | 1 << 16) t.equal((permissions & want), want) }) + +test("hasSomePermissions: detects the permission", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.BanMembers + const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, true) +}) + +test("hasSomePermissions: doesn't detect not the permission", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.SendMessages + const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, false) +}) + +test("hasAllPermissions: detects the permissions", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.BanMembers | DiscordTypes.PermissionFlagsBits.MentionEveryone + const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, true) +}) + +test("hasAllPermissions: doesn't detect not the permissions", t => { + const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.SendMessages | DiscordTypes.PermissionFlagsBits.KickMembers + const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"]) + t.equal(canRemoveMembers, false) +})