Compare commits

...

3 commits

13 changed files with 415 additions and 111 deletions

View file

@ -1,28 +1,54 @@
async function editMessage() { // @ts-check
// Action time!
const passthrough = require("../../passthrough")
const { sync, db } = passthrough
/** @type {import("../converters/edit-to-changes")} */
const editToChanges = sync.require("../converters/edit-to-changes")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/**
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
* @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:", {eventsToRedact, eventsToReplace, eventsToSend})
// 1. Replace all the things. // 1. Replace all the things.
for (const {oldID, newContent} of eventsToReplace) {
const eventType = newContent.$type
/** @type {Pick<typeof newContent, Exclude<keyof newContent, "$type">> & { $type?: string }} */
const newContentWithoutType = {...newContent}
delete newContentWithoutType.$type
await api.sendEvent(roomID, eventType, newContentWithoutType, senderMxid)
// 2. Redact all the things. // Ensure the database is up to date.
// The columns are event_id, event_type, event_subtype, message_id, channel_id, part, source. Only event_subtype could potentially be changed by a replacement event.
// 3. Send all the things. const subtype = newContentWithoutType.msgtype ?? null
db.prepare("UPDATE event_message SET event_subtype = ? WHERE event_id = ?").run(subtype, oldID)
// old code lies here
let eventPart = 0 // TODO: what to do about eventPart when editing? probably just need to make sure that exactly 1 value of '1' remains in the database?
for (const event of events) {
const eventType = event.$type
/** @type {Pick<typeof event, Exclude<keyof event, "$type">> & { $type?: string }} */
const eventWithoutType = {...event}
delete eventWithoutType.$type
const eventID = await api.sendEvent(roomID, eventType, event, senderMxid)
db.prepare("INSERT INTO event_message (event_id, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, 1)").run(eventID, message.id, message.channel_id, eventPart) // source 1 = discord
eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting
eventIDs.push(eventID)
} }
return eventIDs // 2. Redact all the things.
// Not redacting as the last action because the last action is likely to be shown in the room preview in clients, and we don't want it to look like somebody actually deleted a message.
for (const eventID of eventsToRedact) {
await api.redactEvent(roomID, eventID, senderMxid)
// TODO: I should almost certainly remove the redacted event from our database now, shouldn't I? I mean, it's literally not there any more... you can't do anything else with it...
// TODO: If I just redacted part = 0, I should update one of the other events to make it the new part = 0, right?
// TODO: Consider whether this code could be reused between edited messages and deleted messages.
}
{eventsToReplace, eventsToRedact, eventsToSend} // 3. Send all the things.
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
const eventID = await api.sendEvent(roomID, eventType, contentWithoutType, senderMxid)
db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, 1, 1)").run(eventID, eventType, content.msgtype || null, message.id, message.channel_id) // part 1 = supporting; source 1 = discord
}
}
module.exports.editMessage = editMessage

View file

@ -43,7 +43,7 @@ async function createSim(user) {
* Ensure a sim is registered for the user. * Ensure a sim is registered for the user.
* If there is already a sim, use that one. If there isn't one yet, register a new sim. * If there is already a sim, use that one. If there isn't one yet, register a new sim.
* @param {import("discord-api-types/v10").APIUser} user * @param {import("discord-api-types/v10").APIUser} user
* @returns mxid * @returns {Promise<string>} mxid
*/ */
async function ensureSim(user) { async function ensureSim(user) {
let mxid = null let mxid = null
@ -60,7 +60,7 @@ async function ensureSim(user) {
* Ensure a sim is registered for the user and is joined to the room. * Ensure a sim is registered for the user and is joined to the room.
* @param {import("discord-api-types/v10").APIUser} user * @param {import("discord-api-types/v10").APIUser} user
* @param {string} roomID * @param {string} roomID
* @returns mxid * @returns {Promise<string>} mxid
*/ */
async function ensureSimJoined(user, roomID) { async function ensureSimJoined(user, roomID) {
// Ensure room ID is really an ID, not an alias // Ensure room ID is really an ID, not an alias

View file

@ -27,7 +27,7 @@ async function sendMessage(message, guild) {
await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) await registerUser.syncUser(message.author, message.member, message.guild_id, roomID)
} }
const events = await messageToEvent.messageToEvent(message, guild, api) const events = await messageToEvent.messageToEvent(message, guild, {}, {api})
const eventIDs = [] const eventIDs = []
let eventPart = 0 // 0 is primary, 1 is supporting let eventPart = 0 // 0 is primary, 1 is supporting
for (const event of events) { for (const event of events) {

View file

@ -6,8 +6,6 @@ const passthrough = require("../../passthrough")
const { discord, sync, db } = passthrough const { discord, sync, db } = 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("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../actions/register-user")} */ /** @type {import("../actions/register-user")} */
const registerUser = sync.require("../actions/register-user") const registerUser = sync.require("../actions/register-user")
/** @type {import("../actions/create-room")} */ /** @type {import("../actions/create-room")} */
@ -18,8 +16,9 @@ const createRoom = sync.require("../actions/create-room")
* IMPORTANT: This may not have all the normal fields! The API documentation doesn't provide possible types, just says it's all optional! * IMPORTANT: This may not have all the normal fields! The API documentation doesn't provide possible types, just says it's all optional!
* Since I don't have a spec, I will have to capture some real traffic and add it as test cases... I hope they don't change anything later... * Since I don't have a spec, I will have to capture some real traffic and add it as test cases... I hope they don't change anything later...
* @param {import("discord-api-types/v10").APIGuild} guild * @param {import("discord-api-types/v10").APIGuild} guild
* @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API
*/ */
async function editToChanges(message, guild) { async function editToChanges(message, guild, api) {
// Figure out what events we will be replacing // Figure out what events we will be replacing
const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id)
@ -29,7 +28,9 @@ async function editToChanges(message, guild) {
// Figure out what we will be replacing them with // Figure out what we will be replacing them with
const newEvents = await messageToEvent.messageToEvent(message, guild, api) const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api})
const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api})
assert.ok(newFallbackContent.length === newInnerContent.length)
// Match the new events to the old events // Match the new events to the old events
@ -47,35 +48,41 @@ async function editToChanges(message, guild) {
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. This is represented as nothing.
function shift() {
newFallbackContent.shift()
newInnerContent.shift()
}
// For each old event... // For each old event...
outer: while (newEvents.length) { outer: while (newFallbackContent.length) {
const newe = newEvents[0] const newe = newFallbackContent[0]
// Find a new event to pair it with... // Find a new event to pair it with...
for (let i = 0; i < oldEventRows.length; i++) { for (let i = 0; i < oldEventRows.length; i++) {
const olde = oldEventRows[i] const olde = oldEventRows[i]
if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype || null)) { if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype ?? null)) { // The spec does allow subtypes to change, so I can change this condition later if I want to
// Found one! // Found one!
// Set up the pairing // Set up the pairing
eventsToReplace.push({ eventsToReplace.push({
old: olde, old: olde,
new: newe newFallbackContent: newFallbackContent[0],
newInnerContent: newInnerContent[0]
}) })
// These events have been handled now, so remove them from the source arrays // These events have been handled now, so remove them from the source arrays
newEvents.shift() shift()
oldEventRows.splice(i, 1) oldEventRows.splice(i, 1)
// Go all the way back to the start of the next iteration of the outer loop // Go all the way back to the start of the next iteration of the outer loop
continue outer continue outer
} }
} }
// If we got this far, we could not pair it to an existing event, so it'll have to be a new one // If we got this far, we could not pair it to an existing event, so it'll have to be a new one
eventsToSend.push(newe) eventsToSend.push(newInnerContent[0])
newEvents.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
// Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed!
// (Consider a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.) // (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. // 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 => { eventsToReplace = eventsToReplace.filter(ev => {
// Discord does not allow files, images, attachments, or videos to be edited. // Discord does not allow files, images, attachments, or videos to be edited.
@ -92,40 +99,35 @@ async function editToChanges(message, guild) {
// Removing unnecessary properties before returning // Removing unnecessary properties before returning
eventsToRedact = eventsToRedact.map(e => e.event_id) eventsToRedact = eventsToRedact.map(e => e.event_id)
eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, new: eventToReplacementEvent(e.old.event_id, e.new)})) eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)}))
return {eventsToReplace, eventsToRedact, eventsToSend} return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid}
} }
/** /**
* @template T * @template T
* @param {string} oldID * @param {string} oldID
* @param {T} content * @param {T} newFallbackContent
* @param {T} newInnerContent
* @returns {import("../../types").Event.ReplacementContent<T>} content * @returns {import("../../types").Event.ReplacementContent<T>} content
*/ */
function eventToReplacementEvent(oldID, content) { function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) {
const newContent = { const content = {
...content, ...newFallbackContent,
"m.mentions": {}, "m.mentions": {},
"m.new_content": { "m.new_content": {
...content ...newInnerContent
}, },
"m.relates_to": { "m.relates_to": {
rel_type: "m.replace", rel_type: "m.replace",
event_id: oldID event_id: oldID
} }
} }
if (typeof newContent.body === "string") { delete content["m.new_content"]["$type"]
newContent.body = "* " + newContent.body
}
if (typeof newContent.formatted_body === "string") {
newContent.formatted_body = "* " + newContent.formatted_body
}
delete newContent["m.new_content"]["$type"]
// Client-Server API spec 11.37.3: Any m.relates_to property within m.new_content is ignored. // Client-Server API spec 11.37.3: Any m.relates_to property within m.new_content is ignored.
delete newContent["m.new_content"]["m.relates_to"] delete content["m.new_content"]["m.relates_to"]
return newContent return content
} }
module.exports.editToChanges = editToChanges module.exports.editToChanges = editToChanges
module.exports.eventToReplacementEvent = eventToReplacementEvent module.exports.makeReplacementEventContent = makeReplacementEventContent

View file

@ -1,17 +1,35 @@
// @ts-check
const {test} = require("supertape") const {test} = require("supertape")
const {editToChanges} = require("./edit-to-changes") const {editToChanges} = require("./edit-to-changes")
const data = require("../../test/data") const data = require("../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
test("edit2changes: bot response", async t => { test("edit2changes: bot response", async t => {
const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general) const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, {
async getJoinedMembers(roomID) {
t.equal(roomID, "!uCtjHhfGlYbVnPVlkG:cadence.moe")
return new Promise(resolve => {
setTimeout(() => {
resolve({
joined: {
"@cadence:cadence.moe": {
display_name: "cadence [they]",
avatar_url: "whatever"
},
"@_ooye_botrac4r:cadence.moe": {
display_name: "botrac4r",
avatar_url: "whatever"
}
}
})
})
})
}
})
t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToRedact, [])
t.deepEqual(eventsToSend, []) t.deepEqual(eventsToSend, [])
t.deepEqual(eventsToReplace, [{ t.deepEqual(eventsToReplace, [{
oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY",
new: { newContent: {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.text", msgtype: "m.text",
body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
@ -39,17 +57,40 @@ test("edit2changes: bot response", async t => {
}]) }])
}) })
test("edit2changes: remove caption from image", async t => {
const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {})
t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"])
t.deepEqual(eventsToSend, [])
t.deepEqual(eventsToReplace, [])
})
test("edit2changes: add caption back to that image", async t => {
const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {})
t.deepEqual(eventsToRedact, [])
t.deepEqual(eventsToSend, [{
$type: "m.room.message",
msgtype: "m.text",
body: "some text",
"m.mentions": {}
}])
t.deepEqual(eventsToReplace, [])
})
test("edit2changes: edit of reply to skull webp attachment with content", async t => { test("edit2changes: edit of reply to skull webp attachment with content", async t => {
const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_of_reply_to_skull_webp_attachment_with_content, data.guild.general) const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_of_reply_to_skull_webp_attachment_with_content, data.guild.general, {})
t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToRedact, [])
t.deepEqual(eventsToSend, []) t.deepEqual(eventsToSend, [])
t.deepEqual(eventsToReplace, [{ t.deepEqual(eventsToReplace, [{
oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M",
new: { newContent: {
$type: "m.room.message", $type: "m.room.message",
// TODO: read "edits of replies" in the spec!!!
msgtype: "m.text", msgtype: "m.text",
body: "* Edit", body: "> Extremity: Image\n\n* Edit",
format: "org.matrix.custom.html",
formatted_body:
'<mx-reply><blockquote><a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q">In reply to</a> Extremity'
+ '<br>Image</blockquote></mx-reply>'
+ '* Edit',
"m.mentions": {}, "m.mentions": {},
"m.new_content": { "m.new_content": {
msgtype: "m.text", msgtype: "m.text",
@ -60,7 +101,6 @@ test("edit2changes: edit of reply to skull webp attachment with content", async
rel_type: "m.replace", rel_type: "m.replace",
event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M"
} }
// TODO: read "edits of replies" in the spec!!!
} }
}]) }])
}) })

View file

@ -55,9 +55,12 @@ function getDiscordParseCallbacks(message, useHTML) {
/** /**
* @param {import("discord-api-types/v10").APIMessage} message * @param {import("discord-api-types/v10").APIMessage} message
* @param {import("discord-api-types/v10").APIGuild} guild * @param {import("discord-api-types/v10").APIGuild} guild
* @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API * @param {{includeReplyFallback?: boolean, includeEditFallbackStar?: boolean}} options default values:
* - includeReplyFallback: true
* - includeEditFallbackStar: false
* @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API
*/ */
async function messageToEvent(message, guild, api) { async function messageToEvent(message, guild, options = {}, di) {
const events = [] const events = []
/** /**
@ -99,7 +102,7 @@ async function messageToEvent(message, guild, api) {
} }
if (repliedToEventOriginallyFromMatrix) { if (repliedToEventOriginallyFromMatrix) {
// Need to figure out who sent that event... // Need to figure out who sent that event...
const event = await api.getEvent(repliedToEventRoomId, repliedToEventId) const event = await di.api.getEvent(repliedToEventRoomId, repliedToEventId)
repliedToEventSenderMxid = event.sender repliedToEventSenderMxid = event.sender
// Need to add the sender to m.mentions // Need to add the sender to m.mentions
addMention(repliedToEventSenderMxid) addMention(repliedToEventSenderMxid)
@ -133,7 +136,7 @@ async function messageToEvent(message, guild, api) {
if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) {
const writtenMentionsText = matches.map(m => m[1].toLowerCase()) const writtenMentionsText = matches.map(m => m[1].toLowerCase())
const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id)
const {joined} = await api.getJoinedMembers(roomID) const {joined} = await di.api.getJoinedMembers(roomID)
for (const [mxid, member] of Object.entries(joined)) { for (const [mxid, member] of Object.entries(joined)) {
if (!userRegex.some(rx => mxid.match(rx))) { if (!userRegex.some(rx => mxid.match(rx))) {
const localpart = mxid.match(/@([^:]*)/) const localpart = mxid.match(/@([^:]*)/)
@ -143,8 +146,15 @@ async function messageToEvent(message, guild, api) {
} }
} }
// Star * prefix for fallback edits
if (options.includeEditFallbackStar) {
body = "* " + body
html = "* " + html
}
// Fallback body/formatted_body for replies // Fallback body/formatted_body for replies
if (repliedToEventId) { // This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run
if (repliedToEventId && options.includeReplyFallback !== false) {
let repliedToDisplayName let repliedToDisplayName
let repliedToUserHtml let repliedToUserHtml
if (repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid) { if (repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid) {

View file

@ -30,7 +30,7 @@ function mockGetEvent(t, roomID_in, eventID_in, outer) {
} }
test("message2event: simple plaintext", async t => { test("message2event: simple plaintext", async t => {
const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) const events = await messageToEvent(data.message.simple_plaintext, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -40,7 +40,7 @@ test("message2event: simple plaintext", async t => {
}) })
test("message2event: simple user mention", async t => { test("message2event: simple user mention", async t => {
const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) const events = await messageToEvent(data.message.simple_user_mention, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -52,7 +52,7 @@ test("message2event: simple user mention", async t => {
}) })
test("message2event: simple room mention", async t => { test("message2event: simple room mention", async t => {
const events = await messageToEvent(data.message.simple_room_mention, data.guild.general) const events = await messageToEvent(data.message.simple_room_mention, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -64,7 +64,7 @@ test("message2event: simple room mention", async t => {
}) })
test("message2event: simple message link", async t => { test("message2event: simple message link", async t => {
const events = await messageToEvent(data.message.simple_message_link, data.guild.general) const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -76,7 +76,7 @@ test("message2event: simple message link", async t => {
}) })
test("message2event: attachment with no content", async t => { test("message2event: attachment with no content", async t => {
const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) const events = await messageToEvent(data.message.attachment_no_content, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -94,7 +94,7 @@ test("message2event: attachment with no content", async t => {
}) })
test("message2event: stickers", async t => { test("message2event: stickers", async t => {
const events = await messageToEvent(data.message.sticker, data.guild.general) const events = await messageToEvent(data.message.sticker, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -127,7 +127,7 @@ test("message2event: stickers", async t => {
}) })
test("message2event: skull webp attachment with content", async t => { test("message2event: skull webp attachment with content", async t => {
const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general) const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
@ -150,7 +150,7 @@ test("message2event: skull webp attachment with content", async t => {
}) })
test("message2event: reply to skull webp attachment with content", async t => { test("message2event: reply to skull webp attachment with content", async t => {
const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general) const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
"m.relates_to": { "m.relates_to": {
@ -183,15 +183,17 @@ test("message2event: reply to skull webp attachment with content", async t => {
}) })
test("message2event: simple reply to matrix user", async t => { test("message2event: simple reply to matrix user", async t => {
const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, { const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {}, {
getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { api: {
type: "m.room.message", getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", {
content: { type: "m.room.message",
msgtype: "m.text", content: {
body: "so can you reply to my webhook uwu" msgtype: "m.text",
}, body: "so can you reply to my webhook uwu"
sender: "@cadence:cadence.moe" },
}) sender: "@cadence:cadence.moe"
})
}
}) })
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message", $type: "m.room.message",
@ -215,34 +217,66 @@ test("message2event: simple reply to matrix user", async t => {
}]) }])
}) })
test("message2event: simple reply to matrix user, reply fallbacks disabled", async t => {
const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {includeReplyFallback: false}, {
api: {
getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", {
type: "m.room.message",
content: {
msgtype: "m.text",
body: "so can you reply to my webhook uwu"
},
sender: "@cadence:cadence.moe"
})
}
})
t.deepEqual(events, [{
$type: "m.room.message",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4"
}
},
"m.mentions": {
user_ids: [
"@cadence:cadence.moe"
]
},
msgtype: "m.text",
body: "Reply"
}])
})
test("message2event: simple written @mention for matrix user", async t => { test("message2event: simple written @mention for matrix user", async t => {
const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, { const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, {
async getJoinedMembers(roomID) { api: {
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") async getJoinedMembers(roomID) {
return new Promise(resolve => { t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
setTimeout(() => { return new Promise(resolve => {
resolve({ setTimeout(() => {
joined: { resolve({
"@cadence:cadence.moe": { joined: {
display_name: "cadence [they]", "@cadence:cadence.moe": {
avatar_url: "whatever" display_name: "cadence [they]",
}, avatar_url: "whatever"
"@huckleton:cadence.moe": { },
display_name: "huck", "@huckleton:cadence.moe": {
avatar_url: "whatever" display_name: "huck",
}, avatar_url: "whatever"
"@_ooye_botrac4r:cadence.moe": { },
display_name: "botrac4r", "@_ooye_botrac4r:cadence.moe": {
avatar_url: "whatever" display_name: "botrac4r",
}, avatar_url: "whatever"
"@_ooye_bot:cadence.moe": { },
display_name: "Out Of Your Element", "@_ooye_bot:cadence.moe": {
avatar_url: "whatever" display_name: "Out Of Your Element",
avatar_url: "whatever"
}
} }
} })
}) })
}) })
}) }
} }
}) })
t.deepEqual(events, [{ t.deepEqual(events, [{

View file

@ -3,6 +3,9 @@ const {sync, db} = require("../passthrough")
/** @type {import("./actions/send-message")}) */ /** @type {import("./actions/send-message")}) */
const sendMessage = sync.require("./actions/send-message") const sendMessage = sync.require("./actions/send-message")
/** @type {import("./actions/edit-message")}) */
const editMessage = sync.require("./actions/edit-message")
/** @type {import("./actions/add-reaction")}) */ /** @type {import("./actions/add-reaction")}) */
const addReaction = sync.require("./actions/add-reaction") const addReaction = sync.require("./actions/add-reaction")
@ -29,6 +32,25 @@ module.exports = {
sendMessage.sendMessage(message, guild) sendMessage.sendMessage(message, guild)
}, },
/**
* @param {import("./discord-client")} client
* @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message
*/
onMessageUpdate(client, data) {
// Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes.
// If the message content is a string then it includes all interesting fields and is meaningful.
if (typeof data.content === "string") {
/** @type {import("discord-api-types/v10").GatewayMessageCreateDispatchData} */
const message = data
/** @type {import("discord-api-types/v10").APIGuildChannel} */
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 (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first)
editMessage.editMessage(message, guild)
}
},
/** /**
* @param {import("./discord-client")} client * @param {import("./discord-client")} client
* @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data

Binary file not shown.

View file

@ -14,7 +14,7 @@ const makeTxnId = sync.require("./txnid")
/** /**
* @param {string} p endpoint to access * @param {string} p endpoint to access
* @param {string} [mxid] optional: user to act as, for the ?user_id parameter * @param {string?} [mxid] optional: user to act as, for the ?user_id parameter
* @param {{[x: string]: any}} [otherParams] optional: any other query parameters to add * @param {{[x: string]: any}} [otherParams] optional: any other query parameters to add
* @returns {string} the new endpoint * @returns {string} the new endpoint
*/ */
@ -119,7 +119,7 @@ async function sendState(roomID, type, stateKey, content, mxid) {
* @param {string} roomID * @param {string} roomID
* @param {string} type * @param {string} type
* @param {any} content * @param {any} content
* @param {string} [mxid] * @param {string?} [mxid]
* @param {number} [timestamp] timestamp of the newly created event, in unix milliseconds * @param {number} [timestamp] timestamp of the newly created event, in unix milliseconds
*/ */
async function sendEvent(roomID, type, content, mxid, timestamp) { async function sendEvent(roomID, type, content, mxid, timestamp) {
@ -129,6 +129,15 @@ async function sendEvent(roomID, type, content, mxid, timestamp) {
return root.event_id return root.event_id
} }
/**
* @returns {Promise<string>} room ID
*/
async function redactEvent(roomID, eventID, mxid) {
/** @type {Ty.R.EventRedacted} */
const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid))
return root.event_id
}
async function profileSetDisplayname(mxid, displayname) { async function profileSetDisplayname(mxid, displayname) {
await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), { await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), {
displayname displayname
@ -152,5 +161,6 @@ module.exports.getAllState = getAllState
module.exports.getJoinedMembers = getJoinedMembers module.exports.getJoinedMembers = getJoinedMembers
module.exports.sendState = sendState module.exports.sendState = sendState
module.exports.sendEvent = sendEvent module.exports.sendEvent = sendEvent
module.exports.redactEvent = redactEvent
module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetDisplayname = profileSetDisplayname
module.exports.profileSetAvatarUrl = profileSetAvatarUrl module.exports.profileSetAvatarUrl = profileSetAvatarUrl

Binary file not shown.

View file

@ -867,6 +867,162 @@ module.exports = {
tts: false, tts: false,
type: 0 type: 0
}, },
removed_caption_from_image: {
attachments: [
{
content_type: "image/png",
filename: "piper_2.png",
height: 163,
id: "1141501302497615912",
proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1141501302497615912/piper_2.png",
size: 43231,
url: "https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png",
width: 188
}
],
author: {
avatar: "47db1be7ab77e1d812a4573177af0692",
avatar_decoration: null,
discriminator: "0",
global_name: "wing",
id: "112890272819507200",
public_flags: 0,
username: ".wing."
},
channel_id: "112760669178241024",
components: [],
content: "",
edited_timestamp: "2023-08-16T22:38:43.075298+00:00",
embeds: [],
flags: 0,
guild_id: "112760669178241024",
id: "1141501302736695316",
member: {
avatar: null,
communication_disabled_until: null,
deaf: false,
flags: 0,
joined_at: "2015-11-08T12:25:38.461000+00:00",
mute: false,
nick: "windfucker",
pending: false,
premium_since: null,
roles: [
"204427286542417920",
"118924814567211009",
"222168467627835392",
"265239342648131584",
"303273332248412160",
"303319030163439616",
"305775031223320577",
"318243902521868288",
"349185088157777920",
"378402925128712193",
"391076926573510656",
"230462991751970827",
"392141548932038658",
"397533096012152832",
"454567553738473472",
"482658335536185357",
"482860581670486028",
"495384759074160642",
"638988388740890635",
"764071315388629012",
"373336013109461013",
"872274377150980116",
"1034022405275910164",
"790724320824655873",
"1040735082610167858",
"1123730787653660742",
"1070177137367208036"
]
},
mention_everyone: false,
mention_roles: [],
mentions: [],
pinned: false,
timestamp: "2023-08-16T22:38:38.641000+00:00",
tts: false,
type: 0
},
added_caption_to_image: {
attachments: [
{
content_type: "image/png",
filename: "piper_2.png",
height: 163,
id: "1141501302497615912",
proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1141501302497615912/piper_2.png",
size: 43231,
url: "https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png",
width: 188
}
],
author: {
avatar: "47db1be7ab77e1d812a4573177af0692",
avatar_decoration: null,
discriminator: "0",
global_name: "wing",
id: "112890272819507200",
public_flags: 0,
username: ".wing."
},
channel_id: "112760669178241024",
components: [],
content: "some text",
edited_timestamp: "2023-08-17T00:13:18.620975+00:00",
embeds: [],
flags: 0,
guild_id: "112760669178241024",
id: "1141501302736695317",
member: {
avatar: null,
communication_disabled_until: null,
deaf: false,
flags: 0,
joined_at: "2015-11-08T12:25:38.461000+00:00",
mute: false,
nick: "windfucker",
pending: false,
premium_since: null,
roles: [
"204427286542417920",
"118924814567211009",
"222168467627835392",
"265239342648131584",
"303273332248412160",
"303319030163439616",
"305775031223320577",
"318243902521868288",
"349185088157777920",
"378402925128712193",
"391076926573510656",
"230462991751970827",
"392141548932038658",
"397533096012152832",
"454567553738473472",
"482658335536185357",
"482860581670486028",
"495384759074160642",
"638988388740890635",
"764071315388629012",
"373336013109461013",
"872274377150980116",
"1034022405275910164",
"790724320824655873",
"1040735082610167858",
"1123730787653660742",
"1070177137367208036"
]
},
mention_everyone: false,
mention_roles: [],
mentions: [],
pinned: false,
timestamp: "2023-08-16T22:38:38.641000+00:00",
tts: false,
type: 0
},
edit_of_reply_to_skull_webp_attachment_with_content: { edit_of_reply_to_skull_webp_attachment_with_content: {
type: 19, type: 19,
tts: false, tts: false,

4
types.d.ts vendored
View file

@ -112,4 +112,8 @@ namespace R {
export type EventSent = { export type EventSent = {
event_id: string event_id: string
} }
export type EventRedacted = {
event_id: string
}
} }