diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 4b8c691..373a351 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -18,8 +18,8 @@ const createRoom = sync.require("./actions/create-room") const createSpace = sync.require("./actions/create-space") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") -/** @type {import("./discord-command-handler")}) */ -const discordCommandHandler = sync.require("./discord-command-handler") +/** @type {import("../discord/discord-command-handler")}) */ +const discordCommandHandler = sync.require("../discord/discord-command-handler") let lastReportedEvent = 0 diff --git a/d2m/discord-command-handler.js b/discord/discord-command-handler.js similarity index 63% rename from d2m/discord-command-handler.js rename to discord/discord-command-handler.js index fa59822..866999d 100644 --- a/d2m/discord-command-handler.js +++ b/discord/discord-command-handler.js @@ -10,6 +10,8 @@ const {discord, sync, db} = require("../passthrough") const api = sync.require("../matrix/api") /** @type {import("../matrix/file")} */ const file = sync.require("../matrix/file") +/** @type {import("./utils")} */ +const utils = sync.require("./utils") const PREFIX = "//" @@ -121,9 +123,75 @@ const commands = [{ aliases: ["invite"], execute: replyctx( async (message, channel, guild, ctx) => { - return discord.snow.channel.createMessage(channel.id, { + // Check guild is bridged + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) + if (!spaceID || !roomID) return discord.snow.channel.createMessage(channel.id, { ...ctx, - content: "This command isn't implemented yet." + content: "This server isn't bridged to Matrix, so you can't invite Matrix users." + }) + + // Check CREATE_INSTANT_INVITE permission + assert(message.member) + const guildPermissions = utils.getPermissions(message.member.roles, guild.roles) + if (!(guildPermissions & BigInt(1))) { + return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "You don't have permission to invite people to this Discord server." + }) + } + + // Get named MXID + const mxid = message.content.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] + if (!mxid) return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`" + }) + + // Check for existing invite to the space + let spaceMember + try { + spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) + } catch (e) {} + if (spaceMember && spaceMember.membership === "invite") { + return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `\`${mxid}\` already has an invite, which they haven't accepted yet.` + }) + } + + // Invite Matrix user if not in space + if (!spaceMember || spaceMember.membership !== "join") { + await api.inviteToRoom(spaceID, mxid) + return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `You invited \`${mxid}\` to the server.` + }) + } + + // The Matrix user *is* in the space, maybe we want to invite them to this channel? + let roomMember + try { + roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) + } catch (e) {} + if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { + const sent = await discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?\nHit ✅ to make it happen.` + }) + return addButton(channel.id, sent.id, "✅", message.author.id).then(async data => { + await api.inviteToRoom(roomID, mxid) + await discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `You invited \`${mxid}\` to the channel.` + }) + }) + } + + // The Matrix user *is* in the space and in the channel. + await discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `\`${mxid}\` is already in this server and this channel.` }) } ) diff --git a/discord/utils.js b/discord/utils.js new file mode 100644 index 0000000..2a34008 --- /dev/null +++ b/discord/utils.js @@ -0,0 +1,50 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") + +/** + * @param {string[]} userRoles + * @param {DiscordTypes.APIGuild["roles"]} guildRoles + * @param {string} [userID] + * @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channelOverwrites] + */ +function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { + let allowed = BigInt(0) + let everyoneID + // Guild allows + for (const role of guildRoles) { + if (role.name === "@everyone") { + allowed |= BigInt(role.permissions) + everyoneID = role.id + } + if (userRoles.includes(role.id)) { + allowed |= BigInt(role.permissions) + } + } + + if (channelOverwrites) { + /** @type {((overwrite: Required["permission_overwrites"][0]) => any)[]} */ + const actions = [ + // Channel @everyone deny + overwrite => overwrite.id === everyoneID && (allowed &= ~BigInt(overwrite.deny)), + // Channel @everyone allow + overwrite => overwrite.id === everyoneID && (allowed |= BigInt(overwrite.allow)), + // Role deny + overwrite => userRoles.includes(overwrite.id) && (allowed &= ~BigInt(overwrite.deny)), + // Role allow + overwrite => userRoles.includes(overwrite.id) && (allowed |= ~BigInt(overwrite.allow)), + // User deny + overwrite => overwrite.id === userID && (allowed &= ~BigInt(overwrite.deny)), + // User allow + overwrite => overwrite.id === userID && (allowed |= BigInt(overwrite.allow)) + ] + for (let i = 0; i < actions.length; i++) { + for (const overwrite of channelOverwrites) { + actions[i](overwrite) + } + } + } + return allowed +} + +module.exports.getPermissions = getPermissions