forked from cadence/out-of-your-element
Split part and reaction_part
Now, reactions should always end up on the bottom of a message group, instead of sometimes being in the middle.
This commit is contained in:
parent
b7f90db20a
commit
c24752625d
13 changed files with 121 additions and 71 deletions
|
@ -21,7 +21,7 @@ async function addReaction(data) {
|
|||
const user = data.member?.user
|
||||
assert.ok(user && user.username)
|
||||
|
||||
const parentID = select("event_message", "event_id", {message_id: data.message_id, part: 0}).pluck().get() // 0 = primary
|
||||
const parentID = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get()
|
||||
if (!parentID) return // Nothing can be done if the parent message was never bridged.
|
||||
assert.equal(typeof parentID, "string")
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ const api = sync.require("../../matrix/api")
|
|||
* @param {import("discord-api-types/v10").APIGuild} guild
|
||||
*/
|
||||
async function editMessage(message, guild) {
|
||||
const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promoteEvent, promoteNextEvent} = await editToChanges.editToChanges(message, guild, api)
|
||||
const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promotions} = await editToChanges.editToChanges(message, guild, api)
|
||||
|
||||
// 1. Replace all the things.
|
||||
for (const {oldID, newContent} of eventsToReplace) {
|
||||
|
@ -36,24 +36,30 @@ async function editMessage(message, guild) {
|
|||
}
|
||||
|
||||
// 3. Consistency: Ensure there is exactly one part = 0
|
||||
let eventPart = 1
|
||||
if (promoteEvent) {
|
||||
db.prepare("UPDATE event_message SET part = 0 WHERE event_id = ?").run(promoteEvent)
|
||||
} else if (promoteNextEvent) {
|
||||
eventPart = 0
|
||||
const sendNewEventParts = new Set()
|
||||
for (const promotion of promotions) {
|
||||
if ("eventID" in promotion) {
|
||||
db.prepare(`UPDATE event_message SET ${promotion.column} = 0 WHERE event_id = ?`).run(promotion.eventID)
|
||||
} else if ("nextEvent" in promotion) {
|
||||
sendNewEventParts.add(promotion.column)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Send all the things.
|
||||
if (eventsToSend.length) {
|
||||
db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id)
|
||||
}
|
||||
for (const content of eventsToSend) {
|
||||
const eventType = content.$type
|
||||
/** @type {Pick<typeof content, Exclude<keyof content, "$type">> & { $type?: string }} */
|
||||
const contentWithoutType = {...content}
|
||||
delete contentWithoutType.$type
|
||||
delete contentWithoutType.$sender
|
||||
|
||||
const part = sendNewEventParts.has("part") && eventsToSend[0] === content ? 0 : 1
|
||||
const reactionPart = sendNewEventParts.has("reaction_part") && eventsToSend[eventsToSend.length - 1] === content ? 0 : 1
|
||||
const eventID = await api.sendEvent(roomID, eventType, contentWithoutType, senderMxid)
|
||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 1)").run(eventID, eventType, content.msgtype || null, message.id, eventPart) // part 1 = supporting; source 1 = discord
|
||||
|
||||
eventPart = 1
|
||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, content.msgtype || null, message.id, part, reactionPart) // source 1 = discord
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ const converter = sync.require("../converters/remove-reaction")
|
|||
async function removeSomeReactions(data) {
|
||||
const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get()
|
||||
if (!roomID) return
|
||||
const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, part: 0}).pluck().get()
|
||||
const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get()
|
||||
if (!eventIDForMessage) return
|
||||
|
||||
/** @type {Ty.Pagination<Ty.Event.Outer<Ty.Event.M_Reaction>>} */
|
||||
|
|
|
@ -33,12 +33,14 @@ async function sendMessage(message, guild) {
|
|||
|
||||
const events = await messageToEvent.messageToEvent(message, guild, {}, {api})
|
||||
const eventIDs = []
|
||||
let eventPart = 0 // 0 is primary, 1 is supporting
|
||||
if (events.length) {
|
||||
db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id)
|
||||
if (senderMxid) api.sendTyping(roomID, false, senderMxid)
|
||||
}
|
||||
for (const event of events) {
|
||||
const part = event === events[0] ? 0 : 1
|
||||
const reactionPart = event === events[events.length - 1] ? 0 : 1
|
||||
|
||||
const eventType = event.$type
|
||||
if ("$sender" in event) senderMxid = event.$sender
|
||||
/** @type {Pick<typeof event, Exclude<keyof event, "$type" | "$sender">> & { $type?: string, $sender?: string }} */
|
||||
|
@ -48,12 +50,14 @@ async function sendMessage(message, guild) {
|
|||
|
||||
const useTimestamp = message["backfill"] ? new Date(message.timestamp).getTime() : undefined
|
||||
const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, useTimestamp)
|
||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, eventPart) // source 1 = discord
|
||||
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, part, reactionPart) // source 1 = discord
|
||||
|
||||
// The primary event is part = 0 and has the most important and distinct information. It is used to provide reply previews, be pinned, and possibly future uses.
|
||||
// The first event is chosen to be the primary part because it is usually the message text content and is more likely to be distinct.
|
||||
// For example, "Reply to 'this meme made me think of you'" is more useful than "Replied to image".
|
||||
eventPart = 1
|
||||
|
||||
// The last event gets reaction_part = 0. Reactions are managed there because reactions are supposed to appear at the bottom.
|
||||
|
||||
eventIDs.push(eventID)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ async function editToChanges(message, guild, api) {
|
|||
/** @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 oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "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()
|
||||
|
||||
// Figure out what we will be replacing them with
|
||||
|
||||
|
@ -83,17 +83,21 @@ async function editToChanges(message, guild, api) {
|
|||
// Anything remaining in oldEventRows is present in the old version only and should be redacted.
|
||||
eventsToRedact = oldEventRows
|
||||
|
||||
// If events are being deleted, we might be deleting the part = 0. But we want to have a part = 0 at all times. In this case we choose an existing event to promote.
|
||||
let promoteEvent = null, promoteNextEvent = false
|
||||
if (eventsToRedact.some(e => e.part === 0)) {
|
||||
if (eventsToReplace.length) {
|
||||
// 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")
|
||||
eventsToReplace.sort((a, b) => order(b) - order(a))
|
||||
promoteEvent = eventsToReplace[0].old.event_id
|
||||
} else {
|
||||
// Everything is being deleted. Whatever gets sent in their place will be the new part = 0.
|
||||
promoteNextEvent = true
|
||||
// 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})[]} */
|
||||
const promotions = []
|
||||
for (const column of ["part", "reaction_part"]) {
|
||||
// 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 (eventsToReplace.length) {
|
||||
// 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")
|
||||
eventsToReplace.sort((a, b) => order(b) - order(a))
|
||||
promotions.push({column, eventID: eventsToReplace[0].old.event_id})
|
||||
} else {
|
||||
// No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0.
|
||||
promotions.push({column, nextEvent: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +121,7 @@ async function editToChanges(message, guild, api) {
|
|||
eventsToRedact = eventsToRedact.map(e => e.event_id)
|
||||
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, promoteEvent, promoteNextEvent}
|
||||
return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid, promotions}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,7 @@ const data = require("../../test/data")
|
|||
const Ty = require("../../types")
|
||||
|
||||
test("edit2changes: edit by webhook", async t => {
|
||||
const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {})
|
||||
const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {})
|
||||
t.deepEqual(eventsToRedact, [])
|
||||
t.deepEqual(eventsToSend, [])
|
||||
t.deepEqual(eventsToReplace, [{
|
||||
|
@ -27,12 +27,11 @@ test("edit2changes: edit by webhook", async t => {
|
|||
}
|
||||
}])
|
||||
t.equal(senderMxid, null)
|
||||
t.equal(promoteEvent, null)
|
||||
t.equal(promoteNextEvent, false)
|
||||
t.deepEqual(promotions, [])
|
||||
})
|
||||
|
||||
test("edit2changes: bot response", async t => {
|
||||
const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.bot_response, data.guild.general, {
|
||||
const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.bot_response, data.guild.general, {
|
||||
async getJoinedMembers(roomID) {
|
||||
t.equal(roomID, "!hYnGGlPHlbujVVfktC:cadence.moe")
|
||||
return new Promise(resolve => {
|
||||
|
@ -84,21 +83,19 @@ test("edit2changes: bot response", async t => {
|
|||
}
|
||||
}])
|
||||
t.equal(senderMxid, "@_ooye_bojack_horseman:cadence.moe")
|
||||
t.equal(promoteEvent, null)
|
||||
t.equal(promoteNextEvent, false)
|
||||
t.deepEqual(promotions, [])
|
||||
})
|
||||
|
||||
test("edit2changes: remove caption from image", async t => {
|
||||
const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {})
|
||||
const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {})
|
||||
t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"])
|
||||
t.deepEqual(eventsToSend, [])
|
||||
t.deepEqual(eventsToReplace, [])
|
||||
t.equal(promoteEvent, "$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI")
|
||||
t.equal(promoteNextEvent, false)
|
||||
t.deepEqual(promotions, [{column: "part", eventID: "$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI"}])
|
||||
})
|
||||
|
||||
test("edit2changes: change file type", async t => {
|
||||
const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.changed_file_type, data.guild.general, {})
|
||||
const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.changed_file_type, data.guild.general, {})
|
||||
t.deepEqual(eventsToRedact, ["$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ"])
|
||||
t.deepEqual(eventsToSend, [{
|
||||
$type: "m.room.message",
|
||||
|
@ -109,12 +106,11 @@ test("edit2changes: change file type", async t => {
|
|||
msgtype: "m.text"
|
||||
}])
|
||||
t.deepEqual(eventsToReplace, [])
|
||||
t.equal(promoteEvent, null)
|
||||
t.equal(promoteNextEvent, true)
|
||||
t.deepEqual(promotions, [{column: "part", nextEvent: true}, {column: "reaction_part", nextEvent: true}])
|
||||
})
|
||||
|
||||
test("edit2changes: add caption back to that image", async t => {
|
||||
const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {})
|
||||
const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {})
|
||||
t.deepEqual(eventsToRedact, [])
|
||||
t.deepEqual(eventsToSend, [{
|
||||
$type: "m.room.message",
|
||||
|
@ -123,8 +119,7 @@ test("edit2changes: add caption back to that image", async t => {
|
|||
"m.mentions": {}
|
||||
}])
|
||||
t.deepEqual(eventsToReplace, [])
|
||||
t.equal(promoteEvent, null)
|
||||
t.equal(promoteNextEvent, false)
|
||||
t.deepEqual(promotions, [])
|
||||
})
|
||||
|
||||
test("edit2changes: stickers and attachments are not changed, only the content can be edited", async t => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue