Support embed generate MESSAGE_UPDATE events
This commit is contained in:
parent
955310b759
commit
d01c888d02
5 changed files with 117 additions and 51 deletions
|
@ -3,13 +3,22 @@
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select, from} = passthrough
|
const {sync, select, from} = passthrough
|
||||||
/** @type {import("./message-to-event")} */
|
/** @type {import("./message-to-event")} */
|
||||||
const messageToEvent = sync.require("../converters/message-to-event")
|
const messageToEvent = sync.require("../converters/message-to-event")
|
||||||
/** @type {import("../actions/register-user")} */
|
|
||||||
const registerUser = sync.require("../actions/register-user")
|
function eventCanBeEdited(ev) {
|
||||||
/** @type {import("../actions/create-room")} */
|
// Discord does not allow files, images, attachments, or videos to be edited.
|
||||||
const createRoom = sync.require("../actions/create-room")
|
if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote" && ev.old.event_subtype !== "m.notice") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Discord does not allow stickers to be edited.
|
||||||
|
if (ev.old.event_type === "m.sticker") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Anything else is fair game.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
|
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
|
||||||
|
@ -19,12 +28,16 @@ const createRoom = sync.require("../actions/create-room")
|
||||||
* @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API
|
* @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API
|
||||||
*/
|
*/
|
||||||
async function editToChanges(message, guild, api) {
|
async function editToChanges(message, guild, api) {
|
||||||
|
// If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data.
|
||||||
|
|
||||||
|
const isGeneratedEmbed = !("content" in message)
|
||||||
|
|
||||||
// Figure out what events we will be replacing
|
// Figure out what events we will be replacing
|
||||||
|
|
||||||
const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
||||||
assert(roomID)
|
assert(roomID)
|
||||||
/** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */
|
/** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */
|
||||||
const senderMxid = from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null
|
const senderMxid = message.author && from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null
|
||||||
|
|
||||||
const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all()
|
const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all()
|
||||||
|
|
||||||
|
@ -48,7 +61,8 @@ async function editToChanges(message, guild, api) {
|
||||||
let eventsToRedact = []
|
let eventsToRedact = []
|
||||||
/** 3. Events that are present in the new version only, and should be sent as new, with references back to the context */
|
/** 3. Events that are present in the new version only, and should be sent as new, with references back to the context */
|
||||||
let eventsToSend = []
|
let eventsToSend = []
|
||||||
// 4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. This is represented as nothing.
|
/** 4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. */
|
||||||
|
let unchangedEvents = []
|
||||||
|
|
||||||
function shift() {
|
function shift() {
|
||||||
newFallbackContent.shift()
|
newFallbackContent.shift()
|
||||||
|
@ -81,22 +95,35 @@ async function editToChanges(message, guild, api) {
|
||||||
shift()
|
shift()
|
||||||
}
|
}
|
||||||
// Anything remaining in oldEventRows is present in the old version only and should be redacted.
|
// Anything remaining in oldEventRows is present in the old version only and should be redacted.
|
||||||
eventsToRedact = oldEventRows
|
eventsToRedact = oldEventRows.map(e => ({old: e}))
|
||||||
|
|
||||||
|
// If this is a generated embed update, only allow the embeds to be updated, since the system only sends data about events. Ignore changes to other things.
|
||||||
|
if (isGeneratedEmbed) {
|
||||||
|
unchangedEvents.push(...eventsToRedact.filter(e => e.old.event_subtype !== "m.notice")) // Move them from eventsToRedact to unchangedEvents.
|
||||||
|
eventsToRedact = eventsToRedact.filter(e => e.old.event_subtype === "m.notice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed!
|
||||||
|
// (Example: a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.)
|
||||||
|
// So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed.
|
||||||
|
unchangedEvents.push(...eventsToReplace.filter(ev => !eventCanBeEdited(ev))) // Move them from eventsToRedact to unchangedEvents.
|
||||||
|
eventsToReplace = eventsToReplace.filter(eventCanBeEdited)
|
||||||
|
|
||||||
// We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times.
|
// We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times.
|
||||||
/** @type {({column: string, eventID: string} | {column: string, nextEvent: true})[]} */
|
/** @type {({column: string, eventID: string} | {column: string, nextEvent: true})[]} */
|
||||||
const promotions = []
|
const promotions = []
|
||||||
for (const column of ["part", "reaction_part"]) {
|
for (const column of ["part", "reaction_part"]) {
|
||||||
|
const candidatesForParts = unchangedEvents.concat(eventsToReplace)
|
||||||
// If no events with part = 0 exist (or will exist), we need to do some management.
|
// If no events with part = 0 exist (or will exist), we need to do some management.
|
||||||
if (!eventsToReplace.some(e => e.old[column] === 0)) {
|
if (!candidatesForParts.some(e => e.old[column] === 0)) {
|
||||||
if (eventsToReplace.length) {
|
if (candidatesForParts.length) {
|
||||||
// We can choose an existing event to promote. Bigger order is better.
|
// We can choose an existing event to promote. Bigger order is better.
|
||||||
const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.event_subtype === "m.text")
|
const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.old.event_subtype === "m.text")
|
||||||
eventsToReplace.sort((a, b) => order(b) - order(a))
|
candidatesForParts.sort((a, b) => order(b) - order(a))
|
||||||
if (column === "part") {
|
if (column === "part") {
|
||||||
promotions.push({column, eventID: eventsToReplace[0].old.event_id}) // part should be the first one
|
promotions.push({column, eventID: candidatesForParts[0].old.event_id}) // part should be the first one
|
||||||
} else {
|
} else {
|
||||||
promotions.push({column, eventID: eventsToReplace[eventsToReplace.length - 1].old.event_id}) // reaction_part should be the last one
|
promotions.push({column, eventID: candidatesForParts[candidatesForParts.length - 1].old.event_id}) // reaction_part should be the last one
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0.
|
// No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0.
|
||||||
|
@ -105,24 +132,8 @@ async function editToChanges(message, guild, api) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed!
|
|
||||||
// (Example: a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.)
|
|
||||||
// So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed.
|
|
||||||
eventsToReplace = eventsToReplace.filter(ev => {
|
|
||||||
// Discord does not allow files, images, attachments, or videos to be edited.
|
|
||||||
if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote" && ev.old.event_subtype !== "m.notice") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Discord does not allow stickers to be edited.
|
|
||||||
if (ev.old.event_type === "m.sticker") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Anything else is fair game.
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Removing unnecessary properties before returning
|
// Removing unnecessary properties before returning
|
||||||
eventsToRedact = eventsToRedact.map(e => e.event_id)
|
eventsToRedact = eventsToRedact.map(e => e.old.event_id)
|
||||||
eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)}))
|
eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)}))
|
||||||
|
|
||||||
return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid, promotions}
|
return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid, promotions}
|
||||||
|
|
|
@ -235,3 +235,28 @@ test("edit2changes: promotes the text event when multiple rows have part = 1 (sh
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("edit2changes: generated embed", async t => {
|
||||||
|
const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_social_media_image, data.guild.general, {})
|
||||||
|
t.deepEqual(eventsToRedact, [])
|
||||||
|
t.deepEqual(eventsToReplace, [])
|
||||||
|
t.deepEqual(eventsToSend, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
msgtype: "m.notice",
|
||||||
|
body: "| via hthrflwrs on cohost"
|
||||||
|
+ "\n| \n| ## This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO https://cohost.org/jkap/post/4794219-empty"
|
||||||
|
+ "\n| \n| 1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:"
|
||||||
|
+ "\n| \n| * Both players draw eight cards"
|
||||||
|
+ "\n| * Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand"
|
||||||
|
+ "\n| * Both players present their best five-or-less-card pok...",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: `<blockquote><p><sub>hthrflwrs on cohost</sub>`
|
||||||
|
+ `</p><p><strong><a href="https://cohost.org/jkap/post/4794219-empty">This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO</a></strong>`
|
||||||
|
+ `</p><p>1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:`
|
||||||
|
+ `<br><br><ul><li>Both players draw eight cards`
|
||||||
|
+ `</li><li>Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand`
|
||||||
|
+ `</li><li>Both players present their best five-or-less-card pok...</li></ul></p></blockquote>`,
|
||||||
|
"m.mentions": {}
|
||||||
|
}])
|
||||||
|
t.deepEqual(promotions, []) // TODO: it would be ideal to promote this to reaction_part = 0. this is OK to do because the main message won't have had any reactions yet.
|
||||||
|
})
|
||||||
|
|
|
@ -480,6 +480,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
message.content = "changed the channel name to **" + message.content + "**"
|
message.content = "changed the channel name to **" + message.content + "**"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (message.content) {
|
||||||
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
|
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
|
||||||
const matches = [...message.content.matchAll(/@ ?([a-z0-9._]+)\b/gi)]
|
const matches = [...message.content.matchAll(/@ ?([a-z0-9._]+)\b/gi)]
|
||||||
if (matches.length && matches.some(m => m[1].match(/[a-z]/i) && m[1] !== "everyone" && m[1] !== "here")) {
|
if (matches.length && matches.some(m => m[1].match(/[a-z]/i) && m[1] !== "everyone" && m[1] !== "here")) {
|
||||||
|
@ -498,14 +500,15 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text content appears first
|
// Text content appears first
|
||||||
if (message.content) {
|
|
||||||
const {body, html} = await transformContent(message.content)
|
const {body, html} = await transformContent(message.content)
|
||||||
await addTextEvent(body, html, msgtype, {scanMentions: true})
|
await addTextEvent(body, html, msgtype, {scanMentions: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then attachments
|
// Then attachments
|
||||||
|
if (message.attachments) {
|
||||||
const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions)))
|
const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions)))
|
||||||
events.push(...attachmentEvents)
|
events.push(...attachmentEvents)
|
||||||
|
}
|
||||||
|
|
||||||
// Then embeds
|
// Then embeds
|
||||||
for (const embed of message.embeds || []) {
|
for (const embed of message.embeds || []) {
|
||||||
|
|
25
test/data.js
25
test/data.js
|
@ -3469,6 +3469,31 @@ module.exports = {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
guild_id: "112760669178241024"
|
guild_id: "112760669178241024"
|
||||||
|
},
|
||||||
|
embed_generated_social_media_image: {
|
||||||
|
channel_id: "112760669178241024",
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
color: 8594767,
|
||||||
|
description: "1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:\n\n * Both players draw eight cards\n * Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand\n * Both players present their best five-or-less-card pok...",
|
||||||
|
provider: {
|
||||||
|
name: "hthrflwrs on cohost"
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
height: 1587,
|
||||||
|
placeholder: "GpoKP5BJZphshnhwmmmYlmh3l7+m+mwJ",
|
||||||
|
placeholder_version: 1,
|
||||||
|
proxy_url: "https://images-ext-2.discordapp.net/external/9vTXIzlXU4wyUZvWfmlmQkck8nGLUL-A090W4lWsZ48/https/staging.cohostcdn.org/avatar/292-6b64b03c-4ada-42f6-8452-109275bfe68d-profile.png",
|
||||||
|
url: "https://staging.cohostcdn.org/avatar/292-6b64b03c-4ada-42f6-8452-109275bfe68d-profile.png",
|
||||||
|
width: 1644
|
||||||
|
},
|
||||||
|
title: "This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO",
|
||||||
|
type: "link",
|
||||||
|
url: "https://cohost.org/jkap/post/4794219-empty"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
guild_id: "112760669178241024",
|
||||||
|
id: "1210387798297682020"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
special_message: {
|
special_message: {
|
||||||
|
|
|
@ -53,7 +53,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES
|
||||||
('1158842413025071135', '176333891320283136'),
|
('1158842413025071135', '176333891320283136'),
|
||||||
('1197612733600895076', '112760669178241024'),
|
('1197612733600895076', '112760669178241024'),
|
||||||
('1202543413652881428', '1160894080998461480'),
|
('1202543413652881428', '1160894080998461480'),
|
||||||
('1207486471489986620', '1160894080998461480');
|
('1207486471489986620', '1160894080998461480'),
|
||||||
|
('1210387798297682020', '112760669178241024');
|
||||||
|
|
||||||
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
||||||
('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1),
|
('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1),
|
||||||
|
@ -87,7 +88,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
|
||||||
('$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo', 'm.room.message', 'm.text', '1158842413025071135', 0, 0, 1),
|
('$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo', 'm.room.message', 'm.text', '1158842413025071135', 0, 0, 1),
|
||||||
('$7tJoMw1h44n2gxgLUE1T_YinGrLbK0x-TDY1z6M7GBw', 'm.room.message', 'm.text', '1197612733600895076', 0, 0, 1),
|
('$7tJoMw1h44n2gxgLUE1T_YinGrLbK0x-TDY1z6M7GBw', 'm.room.message', 'm.text', '1197612733600895076', 0, 0, 1),
|
||||||
('$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU', 'm.room.message', 'm.text', '1202543413652881428', 0, 0, 0),
|
('$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU', 'm.room.message', 'm.text', '1202543413652881428', 0, 0, 0),
|
||||||
('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0);
|
('$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y', 'm.room.message', 'm.image', '1207486471489986620', 0, 0, 0),
|
||||||
|
('$mPSzglkCu-6cZHbYro0RW2u5mHvbH9aXDjO5FCzosc0', 'm.room.message', 'm.text', '1210387798297682020', 0, 0, 1);
|
||||||
|
|
||||||
INSERT INTO file (discord_url, mxc_url) VALUES
|
INSERT INTO file (discord_url, mxc_url) VALUES
|
||||||
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
||||||
|
|
Loading…
Reference in a new issue