diff --git a/d2m/actions/announce-thread.js b/d2m/actions/announce-thread.js index 546d307..aa6def2 100644 --- a/d2m/actions/announce-thread.js +++ b/d2m/actions/announce-thread.js @@ -4,14 +4,10 @@ const assert = require("assert") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough -/** @type {import("../converters/message-to-event")} */ -const messageToEvent = sync.require("../converters/message-to-event") +/** @type {import("../converters/thread-to-announcement")} */ +const threadToAnnouncement = sync.require("../converters/thread-to-announcement") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") -/** @type {import("./register-user")} */ -const registerUser = sync.require("./register-user") -/** @type {import("../actions/create-room")} */ -const createRoom = sync.require("../actions/create-room") /** * @param {string} parentRoomID @@ -21,24 +17,10 @@ const createRoom = sync.require("../actions/create-room") async function announceThread(parentRoomID, threadRoomID, thread) { /** @type {string?} */ const creatorMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(thread.owner_id) - /** @type {string?} */ - const branchedFromEventID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").get(thread.id) - const msgtype = creatorMxid ? "m.emote" : "m.text" - const template = creatorMxid ? "started a thread:" : "Thread started:" - let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}` - let html = `${template} ${thread.name}` + const content = await threadToAnnouncement.threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, {api}) - const mentions = {} - - await api.sendEvent(parentRoomID, "m.room.message", { - msgtype, - body: `${template} , - format: "org.matrix.custom.html", - formatted_body: "", - "m.mentions": mentions - - }, creatorMxid) + await api.sendEvent(parentRoomID, "m.room.message", content, creatorMxid) } module.exports.announceThread = announceThread diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 2ee0913..4576320 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -70,12 +70,15 @@ async function channelToKState(channel, guild) { avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API } + let history_visibility = "invited" + if (channel["thread_metadata"]) history_visibility = "world_readable" + const channelKState = { "m.room.name/": {name: convertedName}, "m.room.topic/": {topic: convertedTopic}, "m.room.avatar/": avatarEventContent, "m.room.guest_access/": {guest_access: "can_join"}, - "m.room.history_visibility/": {history_visibility: "invited"}, + "m.room.history_visibility/": {history_visibility}, [`m.space.parent/${spaceID}`]: { via: ["cadence.moe"], // TODO: put the proper server here canonical: true @@ -234,19 +237,23 @@ async function _unbridgeRoom(channelID) { * @returns {Promise} */ async function _syncSpaceMember(channel, spaceID, roomID) { + console.error(channel) + console.error("syncing space for", roomID) const spaceKState = await roomToKState(spaceID) let spaceEventContent = {} if ( channel.type !== DiscordTypes.ChannelType.PrivateThread // private threads do not belong in the space (don't offer people something they can't join) - || channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant) + && !channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant) ) { spaceEventContent = { via: ["cadence.moe"] // TODO: use the proper server } } + console.error(spaceEventContent) const spaceDiff = ks.diffKState(spaceKState, { [`m.space.child/${roomID}`]: spaceEventContent }) + console.error(spaceDiff) return applyKStateDiffToRoom(spaceID, spaceDiff) } diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index c34b389..c5da78d 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -65,6 +65,29 @@ function getDiscordParseCallbacks(message, useHTML) { async function messageToEvent(message, guild, options = {}, di) { const events = [] + if (message.type === DiscordTypes.MessageType.ThreadCreated) { + // This is the kind of message that appears when somebody makes a thread which isn't close enough to the message it's based off. + // It lacks the lines and the pill, so it looks kind of like a member join message, and it says: + // [#] NICKNAME started a thread: __THREAD NAME__. __See all threads__ + // We're already bridging the THREAD_CREATED gateway event to make a comparable message, so drop this one. + return [] + } + + if (message.type === DiscordTypes.MessageType.ThreadStarterMessage) { + // This is the message that appears at the top of a thread when the thread was based off an existing message. + // It's just a message reference, no content. + const ref = message.message_reference + assert(ref) + assert(ref.message_id) + const row = db.prepare("SELECT room_id, event_id FROM event_message INNER JOIN channel_room USING (channel_id) WHERE channel_id = ? AND message_id = ?").get(ref.channel_id, ref.message_id) + if (!row) return [] + const event = await di.api.getEvent(row.room_id, row.event_id) + return [{ + ...event.content, + $type: event.type + }] + } + /** @type {{room?: boolean, user_ids?: string[]}} We should consider the following scenarios for mentions: diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 260ecda..062ee0b 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -341,3 +341,25 @@ test("message2event: type 4 channel name change", async t => { formatted_body: "changed the channel name to worming" }]) }) + +test("message2event: thread start message reference", async t => { + const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, { + api: { + getEvent: mockGetEvent(t, "!PnyBKvUBOhjuCucEfk:cadence.moe", "$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo", { + "type": "m.room.message", + "sender": "@_ooye_cadence:cadence.moe", + "content": { + "m.mentions": {}, + "msgtype": "m.text", + "body": "layer 4" + } + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "layer 4", + "m.mentions": {} + }]) +}) diff --git a/d2m/converters/thread-to-announcement.js b/d2m/converters/thread-to-announcement.js new file mode 100644 index 0000000..405f7e9 --- /dev/null +++ b/d2m/converters/thread-to-announcement.js @@ -0,0 +1,46 @@ +// @ts-check + +const assert = require("assert") + +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("../../matrix/read-registration")} */ +const reg = sync.require("../../matrix/read-registration.js") + +const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) + +/** + * @param {string} parentRoomID + * @param {string} threadRoomID + * @param {string?} creatorMxid + * @param {import("discord-api-types/v10").APIThreadChannel} thread + * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API + */ +async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, di) { + /** @type {string?} */ + const branchedFromEventID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").pluck().get(thread.id) + /** @type {{"m.mentions"?: any, "m.in_reply_to"?: any}} */ + const context = {} + if (branchedFromEventID) { + // Need to figure out who sent that event... + const event = await di.api.getEvent(parentRoomID, branchedFromEventID) + context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}} + if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]} + } + + const msgtype = creatorMxid ? "m.emote" : "m.text" + const template = creatorMxid ? "started a thread:" : "Thread started:" + let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}` + let html = `${template} ${thread.name}` + + return { + msgtype, + body, + format: "org.matrix.custom.html", + formatted_body: html, + "m.mentions": {}, + ...context + } +} + +module.exports.threadToAnnouncement = threadToAnnouncement diff --git a/d2m/converters/thread-to-announcement.test.js b/d2m/converters/thread-to-announcement.test.js new file mode 100644 index 0000000..06d937f --- /dev/null +++ b/d2m/converters/thread-to-announcement.test.js @@ -0,0 +1,150 @@ +const {test} = require("supertape") +const {threadToAnnouncement} = require("./thread-to-announcement") +const data = require("../../test/data") +const Ty = require("../../types") + +/** + * @param {string} roomID + * @param {string} eventID + * @returns {(roomID: string, eventID: string) => Promise>} + */ +function mockGetEvent(t, roomID_in, eventID_in, outer) { + return async function(roomID, eventID) { + t.equal(roomID, roomID_in) + t.equal(eventID, eventID_in) + return new Promise(resolve => { + setTimeout(() => { + resolve({ + event_id: eventID_in, + room_id: roomID_in, + origin_server_ts: 1680000000000, + unsigned: { + age: 2245, + transaction_id: "$local.whatever" + }, + ...outer + }) + }) + }) + } +} + +test("thread2announcement: no known creator, no branched from event", async t => { + const content = await threadToAnnouncement("!parent", "!thread", null, { + name: "test thread", + id: "-1" + }) + t.deepEqual(content, { + msgtype: "m.text", + body: "Thread started: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `Thread started: test thread`, + "m.mentions": {} + }) +}) + +test("thread2announcement: known creator, no branched from event", async t => { + const content = await threadToAnnouncement("!parent", "!thread", "@_ooye_crunch_god:cadence.moe", { + name: "test thread", + id: "-1" + }) + t.deepEqual(content, { + msgtype: "m.emote", + body: "started a thread: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `started a thread: test thread`, + "m.mentions": {} + }) +}) + +test("thread2announcement: no known creator, branched from discord event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", null, { + name: "test thread", + id: "1126786462646550579" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", { + type: 'm.room.message', + sender: '@_ooye_bot:cadence.moe', + content: { + msgtype: 'm.text', + body: 'testing testing testing' + } + }) + } + }) + t.deepEqual(content, { + msgtype: "m.text", + body: "Thread started: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `Thread started: test thread`, + "m.mentions": {}, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" + } + } + }) +}) + +test("thread2announcement: known creator, branched from discord event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", "@_ooye_crunch_god:cadence.moe", { + name: "test thread", + id: "1126786462646550579" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", { + type: 'm.room.message', + sender: '@_ooye_bot:cadence.moe', + content: { + msgtype: 'm.text', + body: 'testing testing testing' + } + }) + } + }) + t.deepEqual(content, { + msgtype: "m.emote", + body: "started a thread: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `started a thread: test thread`, + "m.mentions": {}, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" + } + } + }) +}) + +test("thread2announcement: no known creator, branched from matrix event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", null, { + name: "test thread", + id: "1128118177155526666" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + }) + } + }) + t.deepEqual(content, { + msgtype: "m.text", + body: "Thread started: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `Thread started: test thread`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + }, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4" + } + } + }) +}) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 776d4b1..a1a4505 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -86,18 +86,16 @@ const utils = { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) } else if (message.t === "THREAD_CREATE") { - console.log(message) - // await eventDispatcher.onThreadCreate(client, message.d) + // @ts-ignore + await eventDispatcher.onThreadCreate(client, message.d) } else if (message.t === "THREAD_UPDATE") { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, true) } else if (message.t === "MESSAGE_CREATE") { - console.log(message) await eventDispatcher.onMessageCreate(client, message.d) } else if (message.t === "MESSAGE_UPDATE") { - console.log(message) await eventDispatcher.onMessageUpdate(client, message.d) } else if (message.t === "MESSAGE_DELETE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 6e4ffc4..c871ff1 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -10,6 +10,8 @@ const editMessage = sync.require("./actions/edit-message") const deleteMessage = sync.require("./actions/delete-message") /** @type {import("./actions/add-reaction")}) */ const addReaction = sync.require("./actions/add-reaction") +/** @type {import("./actions/announce-thread")}) */ +const announceThread = sync.require("./actions/announce-thread") /** @type {import("./actions/create-room")}) */ const createRoom = sync.require("./actions/create-room") /** @type {import("../matrix/api")}) */ @@ -108,8 +110,7 @@ module.exports = { * @param {import("discord-api-types/v10").APIThreadChannel} thread */ async onThreadCreate(client, thread) { - console.log(thread) - const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id) + const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(thread.parent_id) if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel const threadRoomID = await createRoom.syncRoom(thread.id) // Create room (will share the same inflight as the initial message to the thread) await announceThread.announceThread(parentRoomID, threadRoomID, thread) diff --git a/db/data-for-test.sql b/db/data-for-test.sql index ee31fe3..4a406c9 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -58,7 +58,8 @@ INSERT INTO guild_space (guild_id, space_id) VALUES INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES ('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL), ('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL), -('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL); +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL), +('1100319550446252084', '!PnyBKvUBOhjuCucEfk:cadence.moe', 'worm-farm', NULL, NULL); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), @@ -80,7 +81,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, chan ('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ', 'm.room.message', 'm.image', '1141501302736695317', '112760669178241024', 0, 1), ('$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M', 'm.room.message', 'm.text', '1128084851279536279', '112760669178241024', 0, 1), ('$YUJFa5j0ZJe7PUvD2DykRt9g51RoadUEYmuJLdSEbJ0', 'm.room.message', 'm.image', '1128084851279536279', '112760669178241024', 1, 1), -('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', '112760669178241024', 0, 1); +('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', '112760669178241024', 0, 1), +('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), diff --git a/test/data.js b/test/data.js index fc8cbbd..30d108a 100644 --- a/test/data.js +++ b/test/data.js @@ -1375,6 +1375,63 @@ module.exports = { flags: 0, components: [], position: 12 + }, + updated_to_start_thread_from_here: { + t: "MESSAGE_UPDATE", + s: 19, + op: 0, + d: { + id: "1143121514925928541", + flags: 32, + channel_id: "1100319550446252084", + guild_id: "1100319549670301727" + }, + shard_id: 0 + }, + thread_start_context: { + type: 21, + tts: false, + timestamp: "2023-08-21T09:57:12.558000+00:00", + position: 0, + pinned: false, + message_reference: { + message_id: "1143121514925928541", + guild_id: "1100319549670301727", + channel_id: "1100319550446252084" + }, + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [], + premium_since: null, + pending: false, + nick: "worm", + mute: false, + joined_at: "2023-04-25T07:17:03.696000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1143121620744032327", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "", + components: [], + channel_id: "1143121514925928541", + author: { + username: "cadence.worm", + public_flags: 0, + id: "772659086046658620", + global_name: "cadence", + discriminator: "0", + avatar_decoration_data: null, + avatar: "4b5c4b28051144e4c111f0113a0f1cf1" + }, + attachments: [], + guild_id: "1100319549670301727" } } } diff --git a/test/test.js b/test/test.js index 03394f0..606bd4b 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ require("../matrix/read-registration.test") require("../d2m/converters/message-to-event.test") require("../d2m/converters/message-to-event.embeds.test") require("../d2m/converters/edit-to-changes.test") +require("../d2m/converters/thread-to-announcement.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../d2m/actions/register-user.test")