diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index b46af59..49afca9 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -22,7 +22,7 @@ async function addReaction(data) { assert.equal(typeof parentID, "string") const roomID = await createRoom.ensureRoom(data.channel_id) const senderMxid = await registerUser.ensureSimJoined(user, roomID) - const eventID = await api.sendEvent(roomID, "m.reaction", { + const eventID = api.sendEvent(roomID, "m.reaction", { "m.relates_to": { rel_type: "m.annotation", event_id: parentID, diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 484470e..0fd0646 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -21,8 +21,8 @@ async function roomToKState(roomID) { } /** - * @param {string} roomID - * @param {any} kstate + * @params {string} roomID + * @params {any} kstate */ function applyKStateDiffToRoom(roomID, kstate) { const events = ks.kstateToState(kstate) @@ -51,7 +51,7 @@ function convertNameAndTopic(channel, guild, customName) { } /** - * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel + * @param {DiscordTypes.APIGuildTextChannel} channel * @param {DiscordTypes.APIGuild} guild */ async function channelToKState(channel, guild) { @@ -98,27 +98,21 @@ async function channelToKState(channel, guild) { * @returns {Promise} room ID */ async function createRoom(channel, guild, spaceID, kstate) { - const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) const roomID = await api.createRoom({ - name: convertedName, - topic: convertedTopic, + name: channel.name, + topic: channel.topic || undefined, preset: "private_chat", visibility: "private", invite: ["@cadence:cadence.moe"], // TODO initial_state: ks.kstateToState(kstate) }) - let threadParent = null - if (channel.type === DiscordTypes.ChannelType.PublicThread) { - /** @type {DiscordTypes.APIThreadChannel} */ // @ts-ignore - const thread = channel - threadParent = thread.parent_id - } - - db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) + db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, roomID) // Put the newly created child into the space - _syncSpaceMember(channel, spaceID, roomID) + await api.sendState(spaceID, "m.space.child", roomID, { // TODO: should I deduplicate with the equivalent code from syncRoom? + via: ["cadence.moe"] // TODO: use the proper server + }) return roomID } @@ -162,15 +156,14 @@ async function _syncRoom(channelID, shouldActuallySync) { assert.ok(channel) const guild = channelToGuild(channel) - /** @type {{room_id: string, thread_parent: string?}} */ - const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID) - + /** @type {string?} */ + const existing = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channel.id) if (!existing) { const {spaceID, channelKState} = await channelToKState(channel, guild) return createRoom(channel, guild, spaceID, channelKState) } else { if (!shouldActuallySync) { - return existing.room_id // only need to ensure room exists, and it does. return the room ID + return existing // only need to ensure room exists, and it does. return the room ID } console.log(`[room sync] to matrix: ${channel.name}`) @@ -178,68 +171,24 @@ async function _syncRoom(channelID, shouldActuallySync) { const {spaceID, channelKState} = await channelToKState(channel, guild) // sync channel state to room - const roomKState = await roomToKState(existing.room_id) + const roomKState = await roomToKState(existing) const roomDiff = ks.diffKState(roomKState, channelKState) - const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff) + const roomApply = applyKStateDiffToRoom(existing, roomDiff) // sync room as space member - const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id) + const spaceKState = await roomToKState(spaceID) + const spaceDiff = ks.diffKState(spaceKState, { + [`m.space.child/${existing}`]: { + via: ["cadence.moe"] // TODO: use the proper server + } + }) + const spaceApply = applyKStateDiffToRoom(spaceID, spaceDiff) await Promise.all([roomApply, spaceApply]) - return existing.room_id + return existing } } -async function _unbridgeRoom(channelID) { - /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ - const channel = discord.channels.get(channelID) - assert.ok(channel) - const roomID = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channelID) - assert.ok(roomID) - const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(channel.guild_id) - assert.ok(spaceID) - - // remove room from being a space member - await api.sendState(spaceID, "m.space.child", roomID, {}) - - // send a notification in the room - await api.sendEvent(roomID, "m.room.message", { - msgtype: "m.notice", - body: "⚠️ This room was removed from the bridge." - }) - - // leave room - await api.leaveRoom(roomID) - - // delete room from database - const {changes} = db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channelID) - assert.equal(changes, 1) -} - - -/** - * @param {DiscordTypes.APIGuildTextChannel} channel - * @param {string} spaceID - * @param {string} roomID - * @returns {Promise} - */ -async function _syncSpaceMember(channel, spaceID, 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) - ) { - spaceEventContent = { - via: ["cadence.moe"] // TODO: use the proper server - } - } - const spaceDiff = ks.diffKState(spaceKState, { - [`m.space.child/${roomID}`]: spaceEventContent - }) - return applyKStateDiffToRoom(spaceID, spaceDiff) -} - function ensureRoom(channelID) { return _syncRoom(channelID, false) } @@ -252,11 +201,8 @@ async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) for (const channelID of channelIDs) { - const allowedTypes = [DiscordTypes.ChannelType.GuildText, DiscordTypes.ChannelType.PublicThread] - // @ts-ignore - if (allowedTypes.includes(discord.channels.get(channelID)?.type)) { - const roomID = await syncRoom(channelID) - console.log(`synced ${channelID} <-> ${roomID}`) + if (discord.channels.get(channelID)?.type === DiscordTypes.ChannelType.GuildText) { // TODO: guild sync thread channels and such. maybe make a helper function to check if a given channel is syncable? + await syncRoom(channelID).then(r => console.log(`synced ${channelID}:`, r)) } } } @@ -267,4 +213,3 @@ module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState module.exports._convertNameAndTopic = convertNameAndTopic -module.exports._unbridgeRoom = _unbridgeRoom diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 02c2dcf..e3b6da7 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -1,6 +1,5 @@ // @ts-check -const assert = require("assert") const passthrough = require("../../passthrough") const { sync, db } = passthrough /** @type {import("../../matrix/api")} */ @@ -10,14 +9,13 @@ const api = sync.require("../../matrix/api") * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild */ async function createSpace(guild) { - assert(guild.name) const roomID = await api.createRoom({ name: guild.name, - preset: "private_chat", // cannot join space unless invited + preset: "private_chat", visibility: "private", power_level_content_override: { - events_default: 100, // space can only be managed by bridge - invite: 0 // any existing member can invite others + events_default: 100, + invite: 50 }, invite: ["@cadence:cadence.moe"], // TODO topic: guild.description || undefined, @@ -29,13 +27,13 @@ async function createSpace(guild) { type: "m.room.guest_access", state_key: "", content: { - guest_access: "can_join" // guests can join space if other conditions are met + guest_access: "can_join" } }, { type: "m.room.history_visibility", content: { - history_visibility: "invited" // any events sent after user was invited are visible + history_visibility: "invited" } } ] diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index fa152cf..8e8c838 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -12,7 +12,10 @@ const api = sync.require("../../matrix/api") * @param {import("discord-api-types/v10").APIGuild} guild */ async function editMessage(message, guild) { + console.log(`*** applying edit for message ${message.id} in channel ${message.channel_id}`) const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api) + console.log("making these changes:") + console.dir({eventsToRedact, eventsToReplace, eventsToSend}, {depth: null}) // 1. Replace all the things. for (const {oldID, newContent} of eventsToReplace) { diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index a33cecc..19c3a6d 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -85,8 +85,7 @@ async function ensureSimJoined(user, roomID) { */ async function memberToStateContent(user, member, guildID) { let displayname = user.username - // if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present - if (member.nick) displayname = member.nick + if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present const content = { displayname, diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js index 34470ba..0afce50 100644 --- a/d2m/actions/register-user.test.js +++ b/d2m/actions/register-user.test.js @@ -8,7 +8,7 @@ test("member2state: general", async t => { await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), { avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", - displayname: "The Expert's Submarine", + displayname: "The Expert's Submarine | aprilsong", membership: "join", "moe.cadence.ooye.member": { avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024" diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index a5c8dac..2132905 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -22,11 +22,8 @@ async function sendMessage(message, guild) { let senderMxid = null if (!message.webhook_id) { - if (message.member) { // available on a gateway message create event - senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) - } else { // well, good enough... - senderMxid = await registerUser.ensureSimJoined(message.author, roomID) - } + assert(message.member) + senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) @@ -38,7 +35,7 @@ async function sendMessage(message, guild) { const eventWithoutType = {...event} delete eventWithoutType.$type - const eventID = await api.sendEvent(roomID, eventType, event, senderMxid, new Date(message.timestamp).getTime()) + const eventID = await api.sendEvent(roomID, eventType, event, senderMxid) db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, message.channel_id, eventPart) // source 1 = discord eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js deleted file mode 100644 index 7972f13..0000000 --- a/d2m/converters/message-to-event.embeds.test.js +++ /dev/null @@ -1,40 +0,0 @@ -const {test} = require("supertape") -const {messageToEvent} = require("./message-to-event") -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("message2event embeds: nothing but a field", async t => { - const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {}) - t.deepEqual(events, [{ - $type: "m.room.message", - "m.mentions": {}, - msgtype: "m.text", - body: "Amanda" - }]) -}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index c34b389..29730d4 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -27,13 +27,11 @@ function getDiscordParseCallbacks(message, useHTML) { }, /** @param {{id: string, type: "discordChannel"}} node */ channel: node => { - const row = db.prepare("SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?").get(node.id) - if (!row) { - return `<#${node.id}>` // fallback for when this channel is not bridged - } else if (useHTML) { - return `#${row.nick || row.name}` + const {room_id, name, nick} = db.prepare("SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?").get(node.id) + if (room_id && useHTML) { + return `#${nick || name}` } else { - return `#${row.nick || row.name}` + return `#${nick || name}` } }, /** @param {{animated: boolean, name: string, id: string, type: "discordEmoji"}} node */ @@ -110,13 +108,6 @@ async function messageToEvent(message, guild, options = {}, di) { addMention(repliedToEventSenderMxid) } - let msgtype = "m.text" - // Handle message type 4, channel name changed - if (message.type === DiscordTypes.MessageType.ChannelNameChange) { - msgtype = "m.emote" - message.content = "changed the channel name to **" + message.content + "**" - } - // Text content appears first if (message.content) { let content = message.content @@ -197,7 +188,7 @@ async function messageToEvent(message, guild, options = {}, di) { const newTextMessageEvent = { $type: "m.room.message", "m.mentions": mentions, - msgtype, + msgtype: "m.text", body: body } @@ -248,22 +239,6 @@ async function messageToEvent(message, guild, options = {}, di) { size: attachment.size } } - } else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) { - return { - $type: "m.room.message", - "m.mentions": mentions, - msgtype: "m.video", - url: await file.uploadDiscordFileToMxc(attachment.url), - external_url: attachment.url, - body: attachment.description || attachment.filename, - filename: attachment.filename, - info: { - mimetype: attachment.content_type, - w: attachment.width, - h: attachment.height, - size: attachment.size - } - } } else { return { $type: "m.room.message", diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 260ecda..86942a7 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -330,14 +330,4 @@ test("message2event: very large attachment is linked instead of being uploaded", }]) }) -test("message2event: type 4 channel name change", async t => { - const events = await messageToEvent(data.special_message.thread_name_change, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - "m.mentions": {}, - msgtype: "m.emote", - body: "changed the channel name to **worming**", - format: "org.matrix.custom.html", - formatted_body: "changed the channel name to worming" - }]) -}) +// TODO: read "edits of replies" in the spec diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 1b31260..8c4c430 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -16,10 +16,6 @@ test("user2name: works on emojis", t => { t.equal(userToSimName({username: "🍪 Cookie Monster 🍪", discriminator: "0001"}), "cookie_monster") }) -test("user2name: works on single emoji at the end", t => { - t.equal(userToSimName({username: "Amanda 🎵", discriminator: "2192"}), "amanda") -}) - test("user2name: works on crazy name", t => { t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//") }) @@ -38,4 +34,4 @@ test("user2name: uses ID if name becomes too short", t => { test("user2name: uses ID when name has only disallowed characters", t => { t.equal(userToSimName({username: "!@#$%^&*", discriminator: "0001", id: "9"}), "9") -}) +}) \ No newline at end of file diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 776d4b1..c0ba1a6 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -35,21 +35,6 @@ const utils = { arr.push(channel.id) client.channels.set(channel.id, channel) } - for (const thread of message.d.threads || []) { - // @ts-ignore - thread.guild_id = message.d.id - arr.push(thread.id) - client.channels.set(thread.id, thread) - } - eventDispatcher.checkMissedMessages(client, message.d) - - - } else if (message.t === "THREAD_CREATE") { - client.channels.set(message.d.id, message.d) - - - } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { - client.channels.set(message.d.id, message.d) } else if (message.t === "GUILD_DELETE") { @@ -82,22 +67,10 @@ const utils = { // Event dispatcher for OOYE bridge operations try { - if (message.t === "CHANNEL_UPDATE") { - await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) - - } else if (message.t === "THREAD_CREATE") { - console.log(message) - // 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) + if (message.t === "MESSAGE_CREATE") { 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 154cb34..8e64591 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -10,17 +10,11 @@ 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/create-room")}) */ -const createRoom = sync.require("./actions/create-room") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") let lastReportedEvent = 0 -function isGuildAllowed(guildID) { - return ["112760669178241024", "497159726455455754", "1100319549670301727"].includes(guildID) -} - // Grab Discord events we care about for the bridge, check them, and pass them on module.exports = { @@ -35,94 +29,33 @@ module.exports = { console.error(`while handling this ${gatewayMessage.t} gateway event:`) console.dir(gatewayMessage.d, {depth: null}) - if (Date.now() - lastReportedEvent < 5000) return - lastReportedEvent = Date.now() - - const channelID = gatewayMessage.d.channel_id - if (!channelID) return - const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID) - if (!roomID) return - - let stackLines = e.stack.split("\n") - let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) - if (cloudstormLine !== -1) { - stackLines = stackLines.slice(0, cloudstormLine - 2) - } - api.sendEvent(roomID, "m.room.message", { - msgtype: "m.text", - body: "\u26a0 Bridged event from Discord not delivered. See formatted content for full details.", - format: "org.matrix.custom.html", - formatted_body: "\u26a0 Bridged event from Discord not delivered" - + `
Gateway event: ${gatewayMessage.t}` - + `
${e.toString()}` - + `
Error trace` - + `
${stackLines.join("\n")}
` - + `
Original payload` - + `
${util.inspect(gatewayMessage.d, false, 4, false)}
`, - "m.mentions": { - user_ids: ["@cadence:cadence.moe"] - } - }) - }, - - /** - * When logging back in, check if we missed any conversations in any channels. Bridge up to 49 missed messages per channel. - * If more messages were missed, only the latest missed message will be posted. TODO: Consider bridging more, or post a warning when skipping history? - * This can ONLY detect new messages, not any other kind of event. Any missed edits, deletes, reactions, etc will not be bridged. - * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").GatewayGuildCreateDispatchData} guild - */ - async checkMissedMessages(client, guild) { - if (guild.unavailable) return - const bridgedChannels = db.prepare("SELECT channel_id FROM channel_room").pluck().all() - const prepared = db.prepare("SELECT message_id FROM event_message WHERE channel_id = ? AND message_id = ?").pluck() - for (const channel of guild.channels.concat(guild.threads)) { - if (!bridgedChannels.includes(channel.id)) continue - if (!channel.last_message_id) continue - const latestWasBridged = prepared.get(channel.id, channel.last_message_id) - if (latestWasBridged) continue - - /** More recent messages come first. */ - console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`) - const messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 50}) - let latestBridgedMessageIndex = messages.findIndex(m => { - return prepared.get(channel.id, m.id) - }) - console.log(`[check missed messages] got ${messages.length} messages; last message that IS bridged is at position ${latestBridgedMessageIndex} in the channel`) - if (latestBridgedMessageIndex === -1) latestBridgedMessageIndex = 1 // rather than crawling the ENTIRE channel history, let's just bridge the most recent 1 message to make it up to date. - for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) { - const simulatedGatewayDispatchData = { - guild_id: guild.id, - mentions: [], - ...messages[i] + if (Date.now() - lastReportedEvent > 5000) { + lastReportedEvent = Date.now() + const channelID = gatewayMessage.d.channel_id + if (channelID) { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID) + let stackLines = e.stack.split("\n") + let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) + if (cloudstormLine !== -1) { + stackLines = stackLines.slice(0, cloudstormLine - 2) } - await module.exports.onMessageCreate(client, simulatedGatewayDispatchData) + api.sendEvent(roomID, "m.room.message", { + msgtype: "m.text", + body: "\u26a0 Bridged event from Discord not delivered. See formatted content for full details.", + format: "org.matrix.custom.html", + formatted_body: "\u26a0 Bridged event from Discord not delivered" + + `
Gateway event: ${gatewayMessage.t}` + + `
${stackLines.join("\n")}
` + + `
Original payload` + + `
${util.inspect(gatewayMessage.d, false, 4, false)}
`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + } + }) } } }, - /** - * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").APIChannel} 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) - if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel - await createRoom.syncRoom(thread.id) - }, - - /** - * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread - * @param {boolean} isThread - */ - async onChannelOrThreadUpdate(client, channelOrThread, isThread) { - const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(channelOrThread.id) - if (!roomID) return // No target room to update the data on - await createRoom.syncRoom(channelOrThread.id) - }, - /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message @@ -139,13 +72,13 @@ module.exports = { const channel = client.channels.get(message.channel_id) 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 + if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) await sendMessage.sendMessage(message, guild) }, /** * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} data + * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message */ async onMessageUpdate(client, data) { if (data.webhook_id) { @@ -164,7 +97,7 @@ module.exports = { const channel = client.channels.get(message.channel_id) 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 + if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) await editMessage.editMessage(message, guild) } }, @@ -176,6 +109,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. if (data.emoji.id !== null) return // TODO: image emoji reactions + console.log(data) await addReaction.addReaction(data) }, @@ -184,6 +118,7 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data */ async onMessageDelete(client, data) { + console.log(data) await deleteMessage.deleteMessage(data) } } diff --git a/db/data-for-test.sql b/db/data-for-test.sql index ee31fe3..fa04562 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -33,7 +33,6 @@ CREATE TABLE IF NOT EXISTS "channel_room" ( "room_id" TEXT NOT NULL UNIQUE, "name" TEXT, "nick" TEXT, - "thread_parent" TEXT, PRIMARY KEY("channel_id") ); CREATE TABLE IF NOT EXISTS "event_message" ( @@ -55,10 +54,10 @@ BEGIN TRANSACTION; INSERT INTO guild_space (guild_id, space_id) VALUES ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe'); -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); +INSERT INTO channel_room (channel_id, room_id, name, nick) VALUES +('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main'), +('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL), +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots'); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index 68828dd..d259ddb 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -18,12 +18,10 @@ async function addReaction(event) { // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it let emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions - let encoded = encodeURIComponent(emoji) - let encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "") + emoji = encodeURIComponent(emoji) + emoji = emoji.replace(/%EF%B8%8F/g, "") - console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed) - - return discord.snow.channel.createReaction(channelID, messageID, encoded) + return discord.snow.channel.createReaction(channelID, messageID, emoji) } module.exports.addReaction = addReaction diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index 6d39da7..b62057b 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -41,19 +41,19 @@ async function ensureWebhook(channelID, forceCreate = false) { async function withWebhook(channelID, callback) { const webhook = await ensureWebhook(channelID, false) return callback(webhook).catch(e => { + console.error(e) // TODO: check if the error was webhook-related and if webhook.created === false, then: const webhook = ensureWebhook(channelID, true); return callback(webhook) - throw e + throw new Error(e) }) } /** * @param {string} channelID * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}} data - * @param {string} [threadID] */ -async function sendMessageWithWebhook(channelID, data, threadID) { +async function sendMessageWithWebhook(channelID, data) { const result = await withWebhook(channelID, async webhook => { - return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true}) + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, disableEveryone: true}) }) return result } diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 88ba0fd..3f49fa4 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -13,13 +13,7 @@ const eventToMessage = sync.require("../converters/event-to-message") /** @param {import("../../types").Event.Outer} event */ async function sendEvent(event) { // TODO: we just assume the bridge has already been created - const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id) - let channelID = row.channel_id - let threadID = undefined - if (row.thread_parent) { - threadID = channelID - channelID = row.thread_parent // it's the thread's parent... get with the times... - } + const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(event.room_id) // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it @@ -30,7 +24,7 @@ async function sendEvent(event) { const messageResponses = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const message of messages) { - const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) + const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message) db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content.msgtype || null, messageResponse.id, channelID, eventPart) // source 0 = matrix eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting? diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index b2c56a9..817ffff 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -16,25 +16,11 @@ function eventToMessage(event) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ const messages = [] - let displayName = event.sender - let avatarURL = undefined - const match = event.sender.match(/^@(.*?):/) - if (match) { - displayName = match[1] - // TODO: get the media repo domain and the avatar url from the matrix member event - } - if (event.content.msgtype === "m.text") { messages.push({ content: event.content.body, - username: displayName, - avatar_url: avatarURL - }) - } else if (event.content.msgtype === "m.emote") { - messages.push({ - content: `*${displayName} ${event.content.body}*`, - username: displayName, - avatar_url: avatarURL + username: event.sender.replace(/^@/, ""), + avatar_url: undefined, // TODO: provide the URL to the avatar from the homeserver's content repo }) } diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index f0c4664..e687059 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -21,7 +21,7 @@ test("event2message: janky test", t => { } }), [{ - username: "cadence", + username: "cadence:cadence.moe", content: "test", avatar_url: undefined }] diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 44eba85..82ebd75 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -1,10 +1,9 @@ // @ts-check -/* +/** * Grab Matrix events we care about, check them, and bridge them. */ -const util = require("util") const Ty = require("../types") const {sync, as} = require("../passthrough") @@ -14,58 +13,21 @@ const sendEvent = sync.require("./actions/send-event") const addReaction = sync.require("./actions/add-reaction") /** @type {import("./converters/utils")} */ const utils = sync.require("./converters/utils") -/** @type {import("../matrix/api")}) */ -const api = sync.require("../matrix/api") -let lastReportedEvent = 0 - -function guard(type, fn) { - return async function(event, ...args) { - try { - return await fn(event, ...args) - } catch (e) { - console.error("hit event-dispatcher's error handler with this exception:") - console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? - console.error(`while handling this ${type} gateway event:`) - console.dir(event, {depth: null}) - - if (Date.now() - lastReportedEvent < 5000) return - lastReportedEvent = Date.now() - - let stackLines = e.stack.split("\n") - api.sendEvent(event.room_id, "m.room.message", { - msgtype: "m.text", - body: "\u26a0 Matrix event not delivered to Discord. See formatted content for full details.", - format: "org.matrix.custom.html", - formatted_body: "\u26a0 Matrix event not delivered to Discord" - + `
Event type: ${type}` - + `
${e.toString()}` - + `
Error trace` - + `
${stackLines.join("\n")}
` - + `
Original payload` - + `
${util.inspect(event, false, 4, false)}
`, - "m.mentions": { - user_ids: ["@cadence:cadence.moe"] - } - }) - } - } -} - -sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message", +sync.addTemporaryListener(as, "type:m.room.message", /** * @param {Ty.Event.Outer} event it is a m.room.message because that's what this listener is filtering for */ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) -})) +}) -sync.addTemporaryListener(as, "type:m.reaction", guard("m.reaction", +sync.addTemporaryListener(as, "type:m.reaction", /** * @param {Ty.Event.Outer} event it is a m.reaction because that's what this listener is filtering for */ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return await addReaction.addReaction(event) -})) +}) diff --git a/notes.md b/notes.md index dbc5ecd..ec2b9bb 100644 --- a/notes.md +++ b/notes.md @@ -9,23 +9,6 @@ A database will be used to store the discord id to matrix event id mapping. Tabl There needs to be a way to easily manually trigger something later. For example, it should be easy to manually retry sending a message, or check all members for changes, etc. -## Discord's gateway when a new thread is created from an existing message: - -1. Regular MESSAGE_CREATE of the message that it's going to branch off in the future. Example ID -6423 -2. It MESSAGE_UPDATEd the ID -6423 with this whole data: {id:-6423,flags: 32,channel_id:-2084,guild_id:-1727} (ID is the message ID it's branching off, channel ID is the parent channel containing the message ID it's branching off) -3. It THREAD_CREATEd and gave us a channel object with type 11 (public thread) and parent ID -2084 and ID -6423. -4. It MESSAGE_CREATEd type 21 with blank content and a message reference pointing towards channel -2084 message -6423. (That's the message it branched from in the parent channel.) This MESSAGE_CREATE got ID -4631 (a new ID). Apart from that it's a regular message object. -5. Finally, as the first "real" message in that thread (which a user must send to create that thread!) it sent a regular message object with a new message ID and a channel ID of -6423. - -When viewing this thread, it shows the message branched from at the top, and then the first "real" message right underneath, as separate groups. - -## Current manual process for setting up a server - -1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID)) -2. Call createRoom.createAllForGuild(GUILD_ID) -3. Edit source code of event-dispatcher.js isGuildAllowed() and add the guild ID to the list -4. If developing, make sure SSH port forward is activated, then wait for events to sync over! - ## Transforming content 1. Upload attachments to mxc if they are small enough. diff --git a/stdin.js b/stdin.js index 61a2a08..7e0db89 100644 --- a/stdin.js +++ b/stdin.js @@ -13,7 +13,6 @@ const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const sendEvent = sync.require("./m2d/actions/send-event") -const eventDispatcher = sync.require("./d2m/event-dispatcher") const guildID = "112760669178241024" const extraContext = {} diff --git a/test/data.js b/test/data.js index fc8cbbd..a1d3ece 100644 --- a/test/data.js +++ b/test/data.js @@ -816,130 +816,6 @@ module.exports = { format_type: 1, name: "pomu puff" }] - }, - message_in_thread: { - type: 0, - tts: false, - timestamp: "2023-08-19T01:55:02.063000+00:00", - referenced_message: null, - position: 942, - pinned: false, - nonce: "1142275498206822400", - mentions: [], - mention_roles: [], - mention_everyone: false, - member: { - roles: [ - "112767366235959296", "118924814567211009", - "204427286542417920", "199995902742626304", - "222168467627835392", "238028326281805825", - "259806643414499328", "265239342648131584", - "271173313575780353", "287733611912757249", - "225744901915148298", "305775031223320577", - "318243902521868288", "348651574924541953", - "349185088157777920", "378402925128712193", - "392141548932038658", "393912152173576203", - "482860581670486028", "495384759074160642", - "638988388740890635", "373336013109461013", - "530220455085473813", "454567553738473472", - "790724320824655873", "1123518980456452097", - "1040735082610167858", "695946570482450442", - "1123460940935991296", "849737964090556488" - ], - premium_since: null, - pending: false, - nick: null, - mute: false, - joined_at: "2015-11-11T09:55:40.321000+00:00", - flags: 0, - deaf: false, - communication_disabled_until: null, - avatar: null - }, - id: "1142275501721911467", - flags: 0, - embeds: [], - edited_timestamp: null, - content: "don't mind me, posting something for cadence", - components: [], - channel_id: "910283343378120754", - author: { - username: "kumaccino", - public_flags: 128, - id: "113340068197859328", - global_name: "kumaccino", - discriminator: "0", - avatar_decoration_data: null, - avatar: "b48302623a12bc7c59a71328f72ccb39" - }, - attachments: [], - guild_id: "112760669178241024" - } - }, - message_with_embeds: { - nothing_but_a_field: { - guild_id: "497159726455455754", - mentions: [], - id: "1141934888862351440", - type: 20, - content: "", - channel_id: "497161350934560778", - author: { - id: "1109360903096369153", - username: "Amanda 🎵", - avatar: "d56cd1b26e043ae512edae2214962faa", - discriminator: "2192", - public_flags: 524288, - flags: 524288, - bot: true, - banner: null, - accent_color: null, - global_name: null, - avatar_decoration_data: null, - banner_color: null - }, - attachments: [], - embeds: [ - { - type: "rich", - color: 3092790, - fields: [ - { - name: "Amanda 🎵#2192 <:online:606664341298872324>\nwillow tree, branch 0", - value: "**❯ Uptime:**\n3m 55s\n**❯ Memory:**\n64.45MB", - inline: false - } - ] - } - ], - mention_roles: [], - pinned: false, - mention_everyone: false, - tts: false, - timestamp: "2023-08-18T03:21:33.629000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - application_id: "1109360903096369153", - interaction: { - id: "1141934887608254475", - type: 2, - name: "stats", - user: { - id: "320067006521147393", - username: "papiophidian", - avatar: "47a19b0445069b826e136da4df4259bb", - discriminator: "0", - public_flags: 4194880, - flags: 4194880, - banner: null, - accent_color: null, - global_name: "PapiOphidian", - avatar_decoration_data: null, - banner_color: null - } - }, - webhook_id: "1109360903096369153" } }, message_update: { @@ -1343,38 +1219,5 @@ module.exports = { ], guild_id: "112760669178241024" } - }, - special_message: { - thread_name_change: { - id: "1142391602799710298", - type: 4, - content: "worming", - channel_id: "1142271000067706880", - author: { - id: "772659086046658620", - username: "cadence.worm", - avatar: "4b5c4b28051144e4c111f0113a0f1cf1", - discriminator: "0", - public_flags: 0, - flags: 0, - banner: null, - accent_color: null, - global_name: "cadence", - avatar_decoration_data: null, - banner_color: null - }, - attachments: [], - embeds: [], - mentions: [], - mention_roles: [], - pinned: false, - mention_everyone: false, - tts: false, - timestamp: "2023-08-19T09:36:22.717000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - position: 12 - } } } diff --git a/test/test.js b/test/test.js index 03394f0..31ab099 100644 --- a/test/test.js +++ b/test/test.js @@ -21,7 +21,6 @@ require("../matrix/kstate.test") require("../matrix/api.test") 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/actions/create-room.test") require("../d2m/converters/user-to-mxid.test")