From 58d15d205a18ea25f86340c1b333708afd4911a8 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 12:42:12 +1200 Subject: [PATCH 1/8] don't set the name and topic twice --- d2m/actions/create-room.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index b641d37..cb1bc85 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -118,13 +118,21 @@ async function createRoom(channel, guild, spaceID, kstate) { if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id const invite = threadParent ? [] : ["@cadence:cadence.moe"] // TODO + // Name and topic can be done earlier in room creation rather than in initial_state + // https://spec.matrix.org/latest/client-server-api/#creation + const name = kstate["m.room.name/"].name + delete kstate["m.room.name/"] + assert(name) + const topic = kstate["m.room.topic/"].topic + delete kstate["m.room.topic/"] + assert(topic) + 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", + name, + topic, + preset: "private_chat", // This is closest to what we want, but properties from kstate override it anyway + visibility: "private", // Not shown in the room directory invite, initial_state: ks.kstateToState(kstate) }) From d41062218f32451a94a819c2c1e4412067004a36 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 17:09:25 +1200 Subject: [PATCH 2/8] start adding command handlers --- d2m/discord-command-handler.js | 81 ++++++++++++++++++++++++++++++++++ d2m/event-dispatcher.js | 8 +++- matrix/api.js | 1 + 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 d2m/discord-command-handler.js diff --git a/d2m/discord-command-handler.js b/d2m/discord-command-handler.js new file mode 100644 index 0000000..9f96c18 --- /dev/null +++ b/d2m/discord-command-handler.js @@ -0,0 +1,81 @@ +// @ts-check + +const assert = require("assert").strict +const util = require("util") +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db} = require("../passthrough") +/** @type {import("../matrix/api")}) */ +const api = sync.require("../matrix/api") + +const prefix = "/" + +/** + * @callback CommandExecute + * @param {DiscordTypes.GatewayMessageCreateDispatchData} message + * @param {DiscordTypes.APIGuildTextChannel} channel + * @param {DiscordTypes.APIGuild} guild + * @param {any} [ctx] + */ + +/** + * @typedef Command + * @property {string[]} aliases + * @property {(message: DiscordTypes.GatewayMessageCreateDispatchData, channel: DiscordTypes.APIGuildTextChannel, guild: DiscordTypes.APIGuild) => Promise} execute + */ + +/** @param {CommandExecute} execute */ +function replyctx(execute) { + /** @type {CommandExecute} */ + return function(message, channel, guild, ctx = {}) { + ctx.message_reference = { + message_id: message.id, + channel_id: channel.id, + guild_id: guild.id, + fail_if_not_exists: false + } + return execute(message, channel, guild, ctx) + } +} + +/** @type {Command[]} */ +const commands = [{ + aliases: ["icon", "avatar", "roomicon", "roomavatar", "channelicon", "channelavatar"], + execute: replyctx( + async (message, channel, guild, ctx) => { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) + if (!roomID) return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "This channel isn't bridged to the other side." + }) + const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") + const avatarURL = avatarEvent?.url + return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `Current room avatar: ${avatarURL}` + }) + } + ) +}, { + aliases: ["invite"], + execute: replyctx( + async (message, channel, guild, ctx) => { + discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "This command isn't implemented yet." + }) + } + ) +}] + +/** @type {CommandExecute} */ +async function execute(message, channel, guild) { + if (!message.content.startsWith(prefix)) return + const words = message.content.split(" ") + const commandName = words[0] + const command = commands.find(c => c.aliases.includes(commandName)) + if (!command) return + + await command.execute(message, channel, guild) +} + +module.exports.execute = execute diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 9bb07c0..91a7cba 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -18,6 +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") let lastReportedEvent = 0 @@ -156,7 +158,11 @@ module.exports = { if (!channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) if (!isGuildAllowed(guild.id)) return - await sendMessage.sendMessage(message, guild) + + await Promise.all([ + sendMessage.sendMessage(message, guild), + discordCommandHandler.execute(message, channel, guild) + ]) }, /** diff --git a/matrix/api.js b/matrix/api.js index 2e0763e..7253a1a 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -190,6 +190,7 @@ module.exports.inviteToRoom = inviteToRoom module.exports.leaveRoom = leaveRoom module.exports.getEvent = getEvent module.exports.getAllState = getAllState +module.exports.getStateEvent = getStateEvent module.exports.getJoinedMembers = getJoinedMembers module.exports.sendState = sendState module.exports.sendEvent = sendEvent From e8c172a75386aa9f44aef8e49842c5b842b07a7c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 17:23:32 +1200 Subject: [PATCH 3/8] minor code coverage --- d2m/converters/user-to-mxid.js | 3 ++- m2d/converters/event-to-message.js | 2 +- matrix/txnid.test.js | 12 ++++++++++++ test/test.js | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 matrix/txnid.test.js diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 89e47a4..1fe8ffc 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -39,6 +39,7 @@ function* generateLocalpartAlternatives(preferences) { let i = 2 while (true) { yield best + (i++) + /* c8 ignore next */ } } @@ -69,7 +70,7 @@ function userToSimName(user) { for (const suggestion of generateLocalpartAlternatives(preferences)) { if (!matches.includes(suggestion)) return suggestion } - + /* c8 ignore next */ throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`) } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index b2c56a9..dde77b7 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -32,7 +32,7 @@ function eventToMessage(event) { }) } else if (event.content.msgtype === "m.emote") { messages.push({ - content: `*${displayName} ${event.content.body}*`, + content: `\* _${displayName} ${event.content.body}_`, username: displayName, avatar_url: avatarURL }) diff --git a/matrix/txnid.test.js b/matrix/txnid.test.js new file mode 100644 index 0000000..4db873c --- /dev/null +++ b/matrix/txnid.test.js @@ -0,0 +1,12 @@ +// @ts-check + +const {test} = require("supertape") +const txnid = require("./txnid") + +test("txnid: generates different values each run", t => { + const one = txnid.makeTxnId() + t.ok(one) + const two = txnid.makeTxnId() + t.ok(two) + t.notEqual(two, one) +}) diff --git a/test/test.js b/test/test.js index 606bd4b..e19f8ff 100644 --- a/test/test.js +++ b/test/test.js @@ -20,6 +20,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/read-registration.test") +require("../matrix/txnid.test") require("../d2m/converters/message-to-event.test") require("../d2m/converters/message-to-event.embeds.test") require("../d2m/converters/edit-to-changes.test") From dc3a234038adc79bbcb1c6306b441fef98023ac7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 11:44:58 +1200 Subject: [PATCH 4/8] test on a member that has no member props --- d2m/actions/register-user.test.js | 22 ++++++++++++++++- db/data-for-test.sql | 3 ++- matrix/file.js | 12 +++++----- stdin.js | 1 + test/data.js | 40 +++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js index 34470ba..74818ea 100644 --- a/d2m/actions/register-user.test.js +++ b/d2m/actions/register-user.test.js @@ -3,7 +3,27 @@ const {_memberToStateContent} = require("./register-user") const {test} = require("supertape") const testData = require("../../test/data") -test("member2state: general", async t => { +test("member2state: without member nick or avatar", async t => { + t.deepEqual( + await _memberToStateContent(testData.member.kumaccino.user, testData.member.kumaccino, testData.guild.general.id), + { + avatar_url: "mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL", + displayname: "kumaccino", + membership: "join", + "moe.cadence.ooye.member": { + avatar: "/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024" + }, + "uk.half-shot.discord.member": { + bot: false, + displayColor: 10206929, + id: "113340068197859328", + username: "@kumaccino" + } + } + ) +}) + +test("member2state: with member nick and avatar", async t => { t.deepEqual( await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), { diff --git a/db/data-for-test.sql b/db/data-for-test.sql index ec9f9ec..e88b967 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -93,6 +93,7 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png', 'mxc://cadence.moe/KQYdXKRcHWjDYDLPkTOOWOjA'), ('https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg', 'mxc://cadence.moe/WlAbFSiNRIHPDEwKdyPeGywa'), ('https://cdn.discordapp.com/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024', 'mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl'), -('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'); +('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'), +('https://cdn.discordapp.com/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024', 'mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL'); COMMIT; diff --git a/matrix/file.js b/matrix/file.js index 965ec1c..7d74d5d 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -27,15 +27,15 @@ async function uploadDiscordFileToMxc(path) { } // Are we uploading this file RIGHT NOW? Return the same inflight promise with the same resolution - let existing = inflight.get(url) - if (typeof existing === "string") { - return existing + const existingInflight = inflight.get(url) + if (existingInflight) { + return existingInflight } // Has this file already been uploaded in the past? Grab the existing copy from the database. - existing = db.prepare("SELECT mxc_url FROM file WHERE discord_url = ?").pluck().get(url) - if (typeof existing === "string") { - return existing + const existingFromDb = db.prepare("SELECT mxc_url FROM file WHERE discord_url = ?").pluck().get(url) + if (typeof existingFromDb === "string") { + return existingFromDb } // Download from Discord diff --git a/stdin.js b/stdin.js index ce612f5..a687c6c 100644 --- a/stdin.js +++ b/stdin.js @@ -12,6 +12,7 @@ const createRoom = sync.require("./d2m/actions/create-room") const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") +const file = sync.require("./matrix/file") const sendEvent = sync.require("./m2d/actions/send-event") const eventDispatcher = sync.require("./d2m/event-dispatcher") const ks = sync.require("./matrix/kstate") diff --git a/test/data.js b/test/data.js index 6ed2f42..a4d836d 100644 --- a/test/data.js +++ b/test/data.js @@ -98,6 +98,46 @@ module.exports = { } }, member: { + kumaccino: { + avatar: null, + communication_disabled_until: null, + flags: 0, + joined_at: "2015-11-11T09:55:40.321000+00:00", + nick: null, + pending: false, + premium_since: null, + roles: [ + "112767366235959296", "118924814567211009", + "199995902742626304", "204427286542417920", + "222168467627835392", "238028326281805825", + "259806643414499328", "265239342648131584", + "271173313575780353", "287733611912757249", + "225744901915148298", "305775031223320577", + "318243902521868288", "348651574924541953", + "349185088157777920", "378402925128712193", + "392141548932038658", "393912152173576203", + "482860581670486028", "495384759074160642", + "638988388740890635", "373336013109461013", + "530220455085473813", "454567553738473472", + "790724320824655873", "1040735082610167858", + "695946570482450442", "849737964090556488" + ], + user: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: 10206929, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: "#9bbed1" + }, + mute: false, + deaf: false + }, sheep: { avatar: "38dd359aa12bcd52dd3164126c587f8c", communication_disabled_until: null, From d2c3e7eaa30aba08b43ccae425ad02c082d6dca6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 12:05:16 +1200 Subject: [PATCH 5/8] channel name decoration for threads and voice-text --- d2m/actions/create-room.js | 11 ++++++++--- d2m/actions/create-room.test.js | 29 +++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index cb1bc85..6fbc42e 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -36,12 +36,17 @@ function applyKStateDiffToRoom(roomID, kstate) { } /** - * @param {{id: string, name: string, topic?: string?}} channel + * @param {{id: string, name: string, topic?: string?, type: number}} channel * @param {{id: string}} guild * @param {string?} customName */ function convertNameAndTopic(channel, guild, customName) { - const convertedName = customName || channel.name; + let channelPrefix = + ( channel.type === DiscordTypes.ChannelType.PublicThread ? "[⛓️] " + : channel.type === DiscordTypes.ChannelType.PrivateThread ? "[🔒⛓️] " + : channel.type === DiscordTypes.ChannelType.GuildVoice ? "[🔊] " + : "") + const chosenName = customName || (channelPrefix + channel.name); const maybeTopicWithPipe = channel.topic ? ` | ${channel.topic}` : ''; const maybeTopicWithNewlines = channel.topic ? `${channel.topic}\n\n` : ''; const channelIDPart = `Channel ID: ${channel.id}`; @@ -51,7 +56,7 @@ function convertNameAndTopic(channel, guild, customName) { ? `#${channel.name}${maybeTopicWithPipe}\n\n${channelIDPart}\n${guildIDPart}` : `${maybeTopicWithNewlines}${channelIDPart}\n${guildIDPart}`; - return [convertedName, convertedTopic]; + return [chosenName, convertedTopic]; } /** diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index ec5c3d3..e40bf6f 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -14,28 +14,49 @@ test("channel2room: general", async t => { test("convertNameAndTopic: custom name and topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:"}, {id: "456"}, "hauntings"), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"), ["hauntings", "#the-twilight-zone | Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] ) }) test("convertNameAndTopic: custom name, no topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone"}, {id: "456"}, "hauntings"), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", type: 0}, {id: "456"}, "hauntings"), ["hauntings", "#the-twilight-zone\n\nChannel ID: 123\nGuild ID: 456"] ) }) test("convertNameAndTopic: original name and topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:"}, {id: "456"}, null), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, null), ["the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] ) }) test("convertNameAndTopic: original name, no topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone"}, {id: "456"}, null), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", type: 0}, {id: "456"}, null), ["the-twilight-zone", "Channel ID: 123\nGuild ID: 456"] ) }) + +test("convertNameAndTopic: public thread icon", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 11}, {id: "456"}, null), + ["[⛓️] the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: private thread icon", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 12}, {id: "456"}, null), + ["[🔒⛓️] the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: voice channel icon", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 2}, {id: "456"}, null), + ["[🔊] the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) From d3d9195f72cce90a460e086de1a526b00c9d39c2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 16:01:19 +1200 Subject: [PATCH 6/8] implemented //icon with button confirmation system --- d2m/discord-command-handler.js | 77 ++++++++++++++++++++++++++++++---- d2m/event-dispatcher.js | 1 + 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/d2m/discord-command-handler.js b/d2m/discord-command-handler.js index 9f96c18..1bd52c8 100644 --- a/d2m/discord-command-handler.js +++ b/d2m/discord-command-handler.js @@ -6,15 +6,48 @@ const DiscordTypes = require("discord-api-types/v10") const {discord, sync, db} = require("../passthrough") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") +/** @type {import("../matrix/file")} */ +const file = sync.require("../matrix/file") -const prefix = "/" +const PREFIX = "//" + +let buttons = [] + +/** + * @param {string} channelID where to add the button + * @param {string} messageID where to add the button + * @param {string} emoji emoji to add as a button + * @param {string} userID only listen for responses from this user + * @returns {Promise} + */ +async function addButton(channelID, messageID, emoji, userID) { + await discord.snow.channel.createReaction(channelID, messageID, emoji) + return new Promise(resolve => { + buttons.push({channelID, messageID, userID, resolve, created: Date.now()}) + }) +} + +// Clear out old buttons every so often to free memory +setInterval(() => { + const now = Date.now() + buttons = buttons.filter(b => now - b.created < 2*60*60*1000) +}, 10*60*1000) + +/** @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ +function onReactionAdd(data) { + const button = buttons.find(b => b.channelID === data.channel_id && b.messageID === data.message_id && b.userID === data.user_id) + if (button) { + buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again + button.resolve(data) + } +} /** * @callback CommandExecute * @param {DiscordTypes.GatewayMessageCreateDispatchData} message * @param {DiscordTypes.APIGuildTextChannel} channel * @param {DiscordTypes.APIGuild} guild - * @param {any} [ctx] + * @param {Partial} [ctx] */ /** @@ -42,24 +75,51 @@ const commands = [{ aliases: ["icon", "avatar", "roomicon", "roomavatar", "channelicon", "channelavatar"], execute: replyctx( async (message, channel, guild, ctx) => { + // Guard const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) if (!roomID) return discord.snow.channel.createMessage(channel.id, { ...ctx, content: "This channel isn't bridged to the other side." }) + + // Current avatar const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") - const avatarURL = avatarEvent?.url - return discord.snow.channel.createMessage(channel.id, { + const avatarURLParts = avatarEvent?.url.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + let currentAvatarMessage = + ( avatarURLParts ? `Current room-specific avatar: https://matrix.cadence.moe/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` + : "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.") + + // Next potential avatar + const nextAvatarURL = message.attachments.find(a => a.content_type?.startsWith("image/"))?.url || message.content.match(/https?:\/\/[^ ]+\.[^ ]+\.(?:png|jpg|jpeg|webp)\b/)?.[0] + let nextAvatarMessage = + ( nextAvatarURL ? `\nYou want to set it to: ${nextAvatarURL}\nHit ✅ to make it happen.` + : "") + + const sent = await discord.snow.channel.createMessage(channel.id, { ...ctx, - content: `Current room avatar: ${avatarURL}` + content: currentAvatarMessage + nextAvatarMessage }) + + if (nextAvatarURL) { + addButton(channel.id, sent.id, "✅", message.author.id).then(async data => { + const mxcUrl = await file.uploadDiscordFileToMxc(nextAvatarURL) + await api.sendState(roomID, "m.room.avatar", "", { + url: mxcUrl + }) + db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE channel_id = ?").run(mxcUrl, channel.id) + await discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "Your creation is unleashed. Any complaints will be redirected to Grelbo." + }) + }) + } } ) }, { aliases: ["invite"], execute: replyctx( async (message, channel, guild, ctx) => { - discord.snow.channel.createMessage(channel.id, { + return discord.snow.channel.createMessage(channel.id, { ...ctx, content: "This command isn't implemented yet." }) @@ -69,8 +129,8 @@ const commands = [{ /** @type {CommandExecute} */ async function execute(message, channel, guild) { - if (!message.content.startsWith(prefix)) return - const words = message.content.split(" ") + if (!message.content.startsWith(PREFIX)) return + const words = message.content.slice(PREFIX.length).split(" ") const commandName = words[0] const command = commands.find(c => c.aliases.includes(commandName)) if (!command) return @@ -79,3 +139,4 @@ async function execute(message, channel, guild) { } module.exports.execute = execute +module.exports.onReactionAdd = onReactionAdd diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 91a7cba..bf9bbd2 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -197,6 +197,7 @@ module.exports = { */ async onReactionAdd(client, data) { if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix. + discordCommandHandler.onReactionAdd(data) if (data.emoji.id !== null) return // TODO: image emoji reactions await addReaction.addReaction(data) }, From e9fe2502114592ec8b2e7aa15060f219dd900029 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 16:03:43 +1200 Subject: [PATCH 7/8] preserve order: finish bridge before command reply --- d2m/event-dispatcher.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index bf9bbd2..6939c59 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -159,10 +159,8 @@ module.exports = { const guild = client.guilds.get(channel.guild_id) if (!isGuildAllowed(guild.id)) return - await Promise.all([ - sendMessage.sendMessage(message, guild), - discordCommandHandler.execute(message, channel, guild) - ]) + await sendMessage.sendMessage(message, guild), + await discordCommandHandler.execute(message, channel, guild) }, /** From 1643a46812ed5b96f568e9712ea3bd64750fbf3e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 17:23:51 +1200 Subject: [PATCH 8/8] sync child room avatars when guild is updated --- d2m/actions/create-room.js | 27 ++++++++++++++++----------- d2m/actions/create-space.js | 24 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 6fbc42e..2095130 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -17,6 +17,7 @@ const ks = sync.require("../../matrix/kstate") const inflightRoomCreate = new Map() /** + * Async because it gets all room state from the homeserver. * @param {string} roomID */ async function roomToKState(roomID) { @@ -60,6 +61,7 @@ function convertNameAndTopic(channel, guild, customName) { } /** + * Async because it may upload the guild icon to mxc. * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel * @param {DiscordTypes.APIGuild} guild */ @@ -200,10 +202,10 @@ function channelToGuild(channel) { 3. Get kstate for channel 4. Create room, return new ID - New combined flow with ensure / sync: + Ensure + sync flow: 1. Get IDs 2. Does room exist? - 2.5: If room does exist AND don't need to sync: return here + 2.5: If room does exist AND wasn't asked to sync: return here 3. Get kstate for channel 4. Create room with kstate if room doesn't exist 5. Get and update room state with kstate if room does exist @@ -246,7 +248,7 @@ async function _syncRoom(channelID, shouldActuallySync) { console.log(`[room sync] to matrix: ${channel.name}`) - const {spaceID, channelKState} = await channelToKState(channel, guild) + const {spaceID, channelKState} = await channelToKState(channel, guild) // calling this in both branches because we don't want to calculate this if not syncing // sync channel state to room const roomKState = await roomToKState(roomID) @@ -261,6 +263,16 @@ async function _syncRoom(channelID, shouldActuallySync) { return roomID } +/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. */ +function ensureRoom(channelID) { + return _syncRoom(channelID, false) +} + +/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. */ +function syncRoom(channelID) { + return _syncRoom(channelID, true) +} + async function _unbridgeRoom(channelID) { /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) @@ -289,6 +301,7 @@ async function _unbridgeRoom(channelID) { /** + * Async because it gets all space state from the homeserver, then if necessary sends one state event back. * @param {DiscordTypes.APIGuildTextChannel} channel * @param {string} spaceID * @param {string} roomID @@ -311,14 +324,6 @@ async function _syncSpaceMember(channel, spaceID, roomID) { return applyKStateDiffToRoom(spaceID, spaceDiff) } -function ensureRoom(channelID) { - return _syncRoom(channelID, false) -} - -function syncRoom(channelID) { - return _syncRoom(channelID, true) -} - async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 46fa71f..838bef9 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") @@ -84,11 +84,31 @@ async function syncSpace(guildID) { console.log(`[space sync] to matrix: ${guild.name}`) - // sync channel state to room + // sync guild state to space const spaceKState = await createRoom.roomToKState(spaceID) const spaceDiff = ks.diffKState(spaceKState, guildKState) await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) + // guild icon was changed, so room avatars need to be updated as well as the space ones + // doing it this way rather than calling syncRoom for great efficiency gains + const newAvatarState = spaceDiff["m.room.avatar/"] + if (guild.icon && newAvatarState?.url) { + // don't try to update rooms with custom avatars though + const roomsWithCustomAvatars = db.prepare("SELECT room_id FROM channel_room WHERE custom_avatar IS NOT NULL").pluck().all() + + const childRooms = ks.kstateToState(spaceKState).filter(({type, state_key, content}) => { + return type === "m.space.child" && "via" in content && roomsWithCustomAvatars.includes(state_key) + }).map(({state_key}) => state_key) + + for (const roomID of childRooms) { + const avatarEventContent = await api.getStateEvent(roomID, "m.room.avatar", "") + if (avatarEventContent.url !== newAvatarState.url) { + await api.sendState(roomID, "m.room.avatar", "", newAvatarState) + } + } + } + + return spaceID }