diff --git a/package-lock.json b/package-lock.json
index eb07b4d..ed438d0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "out-of-your-element",
- "version": "3.5.1",
+ "version": "3.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "out-of-your-element",
- "version": "3.5.1",
+ "version": "3.6.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
diff --git a/package.json b/package.json
index 9dfd2a8..73fd43d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "out-of-your-element",
- "version": "3.5.1",
+ "version": "3.6.0",
"description": "A bridge between Matrix and Discord",
"main": "index.js",
"repository": {
diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js
index 8550d43..5b3b4f3 100644
--- a/src/d2m/actions/send-message.js
+++ b/src/d2m/actions/send-message.js
@@ -60,8 +60,7 @@ async function sendMessage(message, channel, guild, row) {
const detailedResultsMessage = await pollEnd.endPoll(message)
if (detailedResultsMessage) {
const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get()
- const channelID = threadParent ? threadParent : message.channel_id
- const threadID = threadParent ? message.channel_id : undefined
+ const {channelID, threadID} = dUtils.swapThreadID(message.channel_id, threadParent)
sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID)
}
}
diff --git a/src/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js
index 218f046..42e3a35 100644
--- a/src/d2m/actions/speedbump.js
+++ b/src/d2m/actions/speedbump.js
@@ -1,6 +1,5 @@
// @ts-check
-const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../../passthrough")
const {discord, select, db} = passthrough
@@ -70,12 +69,18 @@ async function doSpeedbump(messageID) {
* Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted.
* @param {string} channelID
* @param {string} messageID
+ * @param {string} [userID] if provided, only slow down the message when the user has used PK before
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
*/
-async function maybeDoSpeedbump(channelID, messageID) {
- let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
- if (row?.thread_parent) row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
- if (!row?.speedbump_webhook_id) return {affected: false, row: null} // not affected, no speedbump
+async function maybeDoSpeedbump(channelID, messageID, userID) {
+ let row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
+ if (row?.thread_parent) row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
+ if (!row?.speedbump_webhook_id) return {affected: false, row: null} // channel not affected, no speedbump
+ if (userID) {
+ if (row.speedbump_webhook_id === userID) return {affected: false, row} // shortcut
+ const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().get()
+ if (!userHasProxy) return {affected: false, row} // user has not used PK before, no speedbump
+ }
const affected = await doSpeedbump(messageID)
return {affected, row} // maybe affected, and there is a speedbump
}
diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js
index 7229d3d..aeda572 100644
--- a/src/d2m/converters/message-to-event.js
+++ b/src/d2m/converters/message-to-event.js
@@ -265,8 +265,9 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
* @param {any} newEvents merge into events
* @param {any} events will be modified
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
+ * @param {boolean} [forceMerge] if true, must merge event, will error if it had to append
*/
-function mergeTextEvents(newEvents, events, forceSameMsgtype) {
+function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) {
let prev = events.at(-1)
for (const ne of newEvents) {
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
@@ -278,6 +279,8 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype) {
rep.addLine(ne.body, ne.formatted_body)
prev.body = rep.body
prev.formatted_body = rep.formattedBody
+ } else if (forceMerge) {
+ throw new Error("Unable to merge events")
} else {
events.push(ne)
}
@@ -554,7 +557,7 @@ async function messageToEvent(message, guild, options = {}, di) {
// Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter.
// So we scan the message ahead of time for all its emojis and ensure they are in the DB.
- const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)]
+ const emojiMatches = [...content.matchAll(/<(a?):([^:>]+):([0-9]+)>/g)]
await Promise.all(emojiMatches.map(match => {
const id = match[3]
const name = match[2]
@@ -967,7 +970,8 @@ async function messageToEvent(message, guild, options = {}, di) {
// May only be a section accessory or in an action row (up to 5)
if (component.style === DiscordTypes.ButtonStyle.Link) {
assert(component.label) // required for Discord to validate link buttons
- stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `)
+ const link = await transformContentMessageLinks(component.url)
+ stack.msb.add(`[${component.label} ${link}] `, tag`${component.label} `)
}
}
@@ -980,7 +984,19 @@ async function messageToEvent(message, guild, options = {}, di) {
const {body, formatted_body} = stack.msb.get()
if (body.trim().length) {
- await addTextEvent(body, formatted_body, "m.text")
+ // Create new message if Components V2 (cannot have regular content)
+ if ((message.flags ?? 0) & DiscordTypes.MessageFlags.IsComponentsV2) {
+ await addTextEvent(body, formatted_body, "m.text")
+ }
+ // Add to existing message if legacy components https://docs.discord.com/developers/components/reference#legacy-message-component-behavior
+ else {
+ mergeTextEvents([{
+ msgtype: "m.text",
+ body,
+ format: "org.matrix.custom.html",
+ formatted_body
+ }], events, false, true)
+ }
}
}
diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js
index 137b63b..1ef83c3 100644
--- a/src/d2m/converters/message-to-event.test.components.js
+++ b/src/d2m/converters/message-to-event.test.components.js
@@ -1,6 +1,7 @@
const {test} = require("supertape")
const {messageToEvent} = require("./message-to-event")
const data = require("../../../test/data")
+const {mockGetEffectivePower} = require("../../matrix/utils.test")
test("message2event components: pk question mark output", async t => {
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
@@ -77,3 +78,24 @@ test("message2event components: pk question mark output", async t => {
msgtype: "m.text",
}])
})
+
+test("message2event components: pk ping message legacy components", async t => {
+ const events = await messageToEvent(data.message_with_components.pk_ping_components_v1, data.guild.general, {}, {
+ api: {
+ async getJoinedMembers() {
+ return {joined: {}}
+ },
+ getEffectivePower: mockGetEffectivePower()
+ }
+ })
+ t.deepEqual(events, [{
+ $type: "m.room.message",
+ msgtype: "m.text",
+ body: "โญ cadence used `/๐ Ping author`"
+ + "\nPsst, **Red** (@cadence.worm:), you have been pinged by @cadence.worm:."
+ + "\n[Jump https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM?via=cadence.moe] ",
+ format: "org.matrix.custom.html",
+ formatted_body: "
โญ cadence used /๐ Ping author
Psst, Red (@cadence.worm), you have been pinged by @cadence.worm.
Jump ",
+ "m.mentions": {}
+ }])
+})
diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js
index 8101a03..03ca72b 100644
--- a/src/d2m/event-dispatcher.js
+++ b/src/d2m/event-dispatcher.js
@@ -313,7 +313,7 @@ module.exports = {
if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable
- const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id)
+ const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id, message.author.id)
if (affected) return
// @ts-ignore
@@ -335,13 +335,11 @@ module.exports = {
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
// Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from.
- const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
+ const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id, data.author.id)
if (affected) return
- if (!row) {
- // Check that the sending-to room exists, and deal with Eventual Consistency(TM)
- if (!await retrigger.waitForMessage(data.id)) return
- }
+ // Check that the sending-to room exists, and deal with Eventual Consistency(TM)
+ if (!await retrigger.waitForMessage(data.id)) return
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
// @ts-ignore
diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js
index e3d58c4..66012b4 100644
--- a/src/discord/register-interactions.js
+++ b/src/discord/register-interactions.js
@@ -91,40 +91,32 @@ function registerInteractions() {
async function dispatchInteraction(interaction) {
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
try {
- if (interaction.type === DiscordTypes.InteractionType.MessageComponent || interaction.type === DiscordTypes.InteractionType.ModalSubmit) {
- // All we get is custom_id, don't know which context the button was clicked in.
- // So we namespace these ourselves in the custom_id. Currently the only existing namespace is POLL_.
- if (interaction.data.custom_id.startsWith("POLL_")) {
- await poll.interact(interaction)
+ if (interactionId === "Matrix info") {
+ await matrixInfo.interact(interaction)
+ } else if (interactionId === "invite") {
+ await invite.interact(interaction)
+ } else if (interactionId === "invite_channel") {
+ await invite.interactButton(interaction)
+ } else if (interactionId === "Permissions") {
+ await permissions.interact(interaction)
+ } else if (interactionId === "permissions_edit") {
+ await permissions.interactEdit(interaction)
+ } else if (interactionId === "Responses") {
+ /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
+ const messageInteraction = interaction
+ if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) {
+ await pollResponses.interact(messageInteraction)
} else {
- throw new Error(`Unknown message component ${interaction.data.custom_id}`)
+ await reactions.interact(messageInteraction)
}
+ } else if (interactionId === "ping") {
+ await ping.interact(interaction)
+ } else if (interactionId === "privacy") {
+ await privacy.interact(interaction)
+ } else if (interactionId.startsWith("POLL_")) {
+ await poll.interact(interaction)
} else {
- if (interactionId === "Matrix info") {
- await matrixInfo.interact(interaction)
- } else if (interactionId === "invite") {
- await invite.interact(interaction)
- } else if (interactionId === "invite_channel") {
- await invite.interactButton(interaction)
- } else if (interactionId === "Permissions") {
- await permissions.interact(interaction)
- } else if (interactionId === "permissions_edit") {
- await permissions.interactEdit(interaction)
- } else if (interactionId === "Responses") {
- /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
- const messageInteraction = interaction
- if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) {
- await pollResponses.interact(messageInteraction)
- } else {
- await reactions.interact(messageInteraction)
- }
- } else if (interactionId === "ping") {
- await ping.interact(interaction)
- } else if (interactionId === "privacy") {
- await privacy.interact(interaction)
- } else {
- throw new Error(`Unknown interaction ${interactionId}`)
- }
+ throw new Error(`Unknown interaction ${interactionId}`)
}
} catch (e) {
let stackLines = null
diff --git a/src/discord/utils.js b/src/discord/utils.js
index 0d400f1..6fb0b43 100644
--- a/src/discord/utils.js
+++ b/src/discord/utils.js
@@ -182,6 +182,18 @@ function filterTo(xs, fn) {
return filtered
}
+/**
+ * The parameters correspond to the columns of the channel_room table.
+ * @param {string} rowChannelID thread ID, OR channel ID if there is no thread
+ * @param {string | null | undefined} rowThreadParent channel ID if there is a thread
+ */
+function swapThreadID(rowChannelID, rowThreadParent) {
+ return {
+ channelID: rowThreadParent ? rowThreadParent : rowChannelID,
+ threadID: rowThreadParent ? rowChannelID : undefined
+ }
+}
+
const supportedPlaintextPreviewExtensions = new Set([
"4d",
"abnf",
@@ -582,4 +594,5 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
module.exports.filterTo = filterTo
+module.exports.swapThreadID = swapThreadID
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions
diff --git a/src/m2d/actions/vote.js b/src/m2d/actions/vote.js
index 926b957..84f8cc7 100644
--- a/src/m2d/actions/vote.js
+++ b/src/m2d/actions/vote.js
@@ -1,18 +1,12 @@
// @ts-check
const Ty = require("../../types")
-const DiscordTypes = require("discord-api-types/v10")
-const {Readable} = require("stream")
const assert = require("assert").strict
-const crypto = require("crypto")
const passthrough = require("../../passthrough")
-const {sync, discord, db, select} = passthrough
+const {sync, db, select} = passthrough
-const {reg} = require("../../matrix/read-registration")
-/** @type {import("../../matrix/api")} */
-const api = sync.require("../../matrix/api")
-/** @type {import("../../matrix/utils")} */
-const utils = sync.require("../../matrix/utils")
+/** @type {import("../../discord/utils")} */
+const dUtils = sync.require("../../discord/utils")
/** @type {import("../converters/poll-components")} */
const pollComponents = sync.require("../converters/poll-components")
/** @type {import("./channel-webhook")} */
@@ -33,10 +27,11 @@ async function updateVote(event) {
// If poll was started on Matrix, the Discord version is using components, so we can update that to the current status
if (messageRow.source === 0) {
- const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
- assert(channelID)
- await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID))
+ const row = select("channel_room", ["channel_id", "thread_parent"], {room_id: event.room_id}).get()
+ assert(row)
+ const {channelID, threadID} = dUtils.swapThreadID(row.channel_id, row.thread_parent)
+ await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID), threadID)
}
}
-module.exports.updateVote = updateVote
\ No newline at end of file
+module.exports.updateVote = updateVote
diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug
index 9791ae3..7411a1e 100644
--- a/src/web/pug/guild.pug
+++ b/src/web/pug/guild.pug
@@ -122,7 +122,7 @@ block body
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
.s-popover--arrow.s-popover--arrow__tc
+add-roles-menu(guild, guild_id)
- p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
+ p.fc-light.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
h3.mt32.fs-category Features
.s-card.d-grid.px0.g16
@@ -191,14 +191,14 @@ block body
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
+discord(channel, true, "Announcement")
else
- .s-empty-state.p8 All Discord channels are linked.
+ .s-empty-state.p8 No Discord channels available.
.fl-grow1.s-btn-group.fd-column.w30
each room in unlinkedRooms
input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id)
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
+matrix(room, true)
else
- .s-empty-state.p8 All Matrix rooms are linked.
+ .s-empty-state.p8 No Matrix rooms available.
input(type="hidden" name="guild_id" value=guild_id)
div
button.s-btn.s-btn__icon.s-btn__filled#link-button
diff --git a/test/data.js b/test/data.js
index f3092bc..eab9a63 100644
--- a/test/data.js
+++ b/test/data.js
@@ -5473,6 +5473,189 @@ module.exports = {
content: '-# Original Message ID: 1466556003645657118 ยท '
}
]
+ },
+ pk_ping_components_v1: {
+ type: 23,
+ content: "Psst, **Red** (<@772659086046658620>), you have been pinged by <@772659086046658620>.",
+ mentions: [
+ {
+ id: "772659086046658620",
+ username: "cadence.worm",
+ avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
+ discriminator: "0",
+ public_flags: 0,
+ flags: 0,
+ banner: null,
+ accent_color: null,
+ global_name: "cadence",
+ avatar_decoration_data: null,
+ collectibles: null,
+ display_name_styles: null,
+ banner_color: null,
+ clan: {
+ identity_guild_id: "532245108070809601",
+ identity_enabled: true,
+ tag: "doll",
+ badge: "dba08126b4e810a0e096cc7cd5bc37f0"
+ },
+ primary_guild: {
+ identity_guild_id: "532245108070809601",
+ identity_enabled: true,
+ tag: "doll",
+ badge: "dba08126b4e810a0e096cc7cd5bc37f0"
+ }
+ }
+ ],
+ mention_roles: [],
+ attachments: [],
+ embeds: [],
+ timestamp: "2026-03-25T07:07:02.626000+00:00",
+ edited_timestamp: null,
+ flags: 0,
+ components: [
+ {
+ type: 1,
+ id: 1,
+ components: [
+ {
+ type: 2,
+ id: 2,
+ style: 5,
+ label: "Jump",
+ url: "https://discord.com/channels/1160893336324931584/1160894080998461480/1440549403667468320"
+ }
+ ]
+ }
+ ],
+ id: "1486260105908457653",
+ channel_id: "1160894080998461480",
+ author: {
+ id: "466378653216014359",
+ username: "PluralKit",
+ avatar: "b78ef67a081737a830b60aa47d9ebcd9",
+ discriminator: "4020",
+ public_flags: 65536,
+ flags: 65536,
+ bot: true,
+ banner: null,
+ accent_color: null,
+ global_name: null,
+ avatar_decoration_data: null,
+ collectibles: null,
+ display_name_styles: null,
+ banner_color: null,
+ clan: null,
+ primary_guild: null
+ },
+ pinned: false,
+ mention_everyone: false,
+ tts: false,
+ application_id: "466378653216014359",
+ interaction: {
+ id: "1486260103928614932",
+ type: 2,
+ name: "๐ Ping author",
+ user: {
+ id: "772659086046658620",
+ username: "cadence.worm",
+ avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
+ discriminator: "0",
+ public_flags: 0,
+ flags: 0,
+ banner: null,
+ accent_color: null,
+ global_name: "cadence",
+ avatar_decoration_data: null,
+ collectibles: null,
+ display_name_styles: null,
+ banner_color: null,
+ clan: {
+ identity_guild_id: "532245108070809601",
+ identity_enabled: true,
+ tag: "doll",
+ badge: "dba08126b4e810a0e096cc7cd5bc37f0"
+ },
+ primary_guild: {
+ identity_guild_id: "532245108070809601",
+ identity_enabled: true,
+ tag: "doll",
+ badge: "dba08126b4e810a0e096cc7cd5bc37f0"
+ }
+ }
+ },
+ webhook_id: "466378653216014359",
+ message_reference: {
+ type: 0,
+ channel_id: "1160894080998461480",
+ message_id: "1440549403667468320",
+ guild_id: "1160893336324931584"
+ },
+ interaction_metadata: {
+ id: "1486260103928614932",
+ type: 2,
+ user: {
+ id: "772659086046658620",
+ username: "cadence.worm",
+ avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
+ discriminator: "0",
+ public_flags: 0,
+ flags: 0,
+ banner: null,
+ accent_color: null,
+ global_name: "cadence",
+ avatar_decoration_data: null,
+ collectibles: null,
+ display_name_styles: null,
+ banner_color: null,
+ clan: {
+ identity_guild_id: "532245108070809601",
+ identity_enabled: true,
+ tag: "doll",
+ badge: "dba08126b4e810a0e096cc7cd5bc37f0"
+ },
+ primary_guild: {
+ identity_guild_id: "532245108070809601",
+ identity_enabled: true,
+ tag: "doll",
+ badge: "dba08126b4e810a0e096cc7cd5bc37f0"
+ }
+ },
+ authorizing_integration_owners: { "0": "1160893336324931584" },
+ name: "๐ Ping author",
+ command_type: 3,
+ target_message_id: "1440549403667468320"
+ },
+ referenced_message: {
+ type: 0,
+ content: "test",
+ mentions: [],
+ mention_roles: [],
+ attachments: [],
+ embeds: [],
+ timestamp: "2025-11-19T03:49:01.948000+00:00",
+ edited_timestamp: null,
+ flags: 0,
+ components: [],
+ id: "1440549403667468320",
+ channel_id: "1160894080998461480",
+ author: {
+ id: "1195662438662680720",
+ username: "special name",
+ avatar: "a82347890f2739e5880cd82b8c1a708e",
+ discriminator: "0000",
+ public_flags: 0,
+ flags: 0,
+ bot: true,
+ global_name: null,
+ clan: null,
+ primary_guild: null
+ },
+ pinned: false,
+ mention_everyone: false,
+ tts: false,
+ application_id: "466378653216014359",
+ webhook_id: "1195662438662680720"
+ }
}
},
message_update: {
diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql
index 8dd71cd..1662320 100644
--- a/test/ooye-test-data.sql
+++ b/test/ooye-test-data.sql
@@ -95,7 +95,8 @@ WITH a (message_id, channel_id) AS (VALUES
('1381212840957972480', '112760669178241024'),
('1401760355339862066', '112760669178241024'),
('1439351590262800565', '1438284564815548418'),
-('1404133238414376971', '112760669178241024'))
+('1404133238414376971', '112760669178241024'),
+('1440549403667468320', '1160894080998461480'))
SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id;
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
@@ -143,7 +144,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
-('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
+('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1),
+('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1);
INSERT INTO file (discord_url, mxc_url) VALUES
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),