Compare commits

...

6 commits

8 changed files with 422 additions and 50 deletions

View file

@ -47,6 +47,13 @@ CREATE TABLE IF NOT EXISTS "event_message" (
"source" INTEGER NOT NULL,
PRIMARY KEY("event_id","message_id")
);
CREATE TABLE IF NOT EXISTS "member_cache" (
"room_id" TEXT NOT NULL,
"mxid" TEXT NOT NULL,
"displayname" TEXT,
"avatar_url" TEXT,
PRIMARY KEY("room_id", "mxid")
);
COMMIT;
@ -67,7 +74,8 @@ INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES
('820865262526005258', 'crunch_god', '_ooye_crunch_god', '@_ooye_crunch_god:cadence.moe'),
('771520384671416320', 'bojack_horseman', '_ooye_bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'),
('112890272819507200', '.wing.', '_ooye_.wing.', '@_ooye_.wing.:cadence.moe'),
('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe');
('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'),
('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe');;
INSERT INTO sim_member (mxid, room_id, profile_event_content_hash) VALUES
('@_ooye_bojack_horseman:cadence.moe', '!uCtjHhfGlYbVnPVlkG:cadence.moe', NULL);
@ -86,7 +94,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, chan
('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1),
('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', '122155380120748034', 0, 1),
('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', '122155380120748034', 0, 0),
('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0);
('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0),
('$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04', 'm.room.message', 'm.text', '1144865310588014633', '687028734322147344', 0, 1);
INSERT INTO file (discord_url, mxc_url) VALUES
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
@ -99,4 +108,9 @@ INSERT INTO file (discord_url, mxc_url) VALUES
('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'),
('https://cdn.discordapp.com/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024', 'mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL');
INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES
('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),
('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU');
COMMIT;

View file

@ -21,9 +21,20 @@ async function addReaction(event) {
let encoded = encodeURIComponent(emoji)
let encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "")
console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed)
// https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ????????????
return discord.snow.channel.createReaction(channelID, messageID, encoded)
const forceTrimmedList = [
"%E2%AD%90" // ⭐
]
let discordPreferredEncoding =
( forceTrimmedList.includes(encodedTrimmed) ? encodedTrimmed
: encodedTrimmed !== encoded && [...emoji].length === 2 ? encoded
: encodedTrimmed)
console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed, "chosen:", discordPreferredEncoding)
return discord.snow.channel.createReaction(channelID, messageID, discordPreferredEncoding)
}
module.exports.addReaction = addReaction

View file

@ -9,6 +9,8 @@ const {sync, discord, db} = passthrough
const channelWebhook = sync.require("./channel-webhook")
/** @type {import("../converters/event-to-message")} */
const eventToMessage = sync.require("../converters/event-to-message")
/** @type {import("../../matrix/api")}) */
const api = sync.require("../../matrix/api")
/** @param {import("../../types").Event.Outer<any>} event */
async function sendEvent(event) {
@ -20,10 +22,14 @@ async function sendEvent(event) {
threadID = channelID
channelID = row.thread_parent // it's the thread's parent... get with the times...
}
// @ts-ignore
const guildID = discord.channels.get(channelID).guild_id
const guild = discord.guilds.get(guildID)
assert(guild)
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
const messages = eventToMessage.eventToMessage(event)
const messages = await eventToMessage.eventToMessage(event, guild, {api})
assert(Array.isArray(messages)) // sanity
/** @type {DiscordTypes.APIMessage[]} */

View file

@ -9,6 +9,8 @@ const passthrough = require("../../passthrough")
const { sync, db, discord } = passthrough
/** @type {import("../../matrix/file")} */
const file = sync.require("../../matrix/file")
/** @type {import("../converters/utils")} */
const utils = sync.require("../converters/utils")
const BLOCK_ELEMENTS = [
"ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS",
@ -26,6 +28,8 @@ const turndownService = new TurndownService({
codeBlockStyle: "fenced"
})
turndownService.remove("mx-reply")
turndownService.addRule("strikethrough", {
filter: ["del", "s", "strike"],
replacement: function (content) {
@ -33,20 +37,75 @@ turndownService.addRule("strikethrough", {
}
})
turndownService.addRule("blockquote", {
filter: "blockquote",
replacement: function (content) {
content = content.replace(/^\n+|\n+$/g, "")
content = content.replace(/^/gm, "> ")
return content
}
})
turndownService.addRule("fencedCodeBlock", {
filter: function (node, options) {
return (
options.codeBlockStyle === "fenced" &&
node.nodeName === "PRE" &&
node.firstChild &&
node.firstChild.nodeName === "CODE"
)
},
replacement: function (content, node, options) {
const className = node.firstChild.getAttribute("class") || ""
const language = (className.match(/language-(\S+)/) || [null, ""])[1]
const code = node.firstChild
const visibleCode = code.childNodes.map(c => c.nodeName === "BR" ? "\n" : c.textContent).join("").replace(/\n*$/g, "")
var fence = "```"
return (
fence + language + "\n" +
visibleCode +
"\n" + fence
)
}
})
/**
* @param {string} roomID
* @param {string} mxid
* @returns {Promise<{displayname?: string?, avatar_url?: string?}>}
*/
async function getMemberFromCacheOrHomeserver(roomID, mxid, api) {
const row = db.prepare("SELECT displayname, avatar_url FROM member_cache WHERE room_id = ? AND mxid = ?").get(roomID, mxid)
if (row) return row
return api.getStateEvent(roomID, "m.room.member", mxid).then(event => {
db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(roomID, mxid, event?.displayname || null, event?.avatar_url || null)
return event
}).catch(() => {
return {displayname: null, avatar_url: null}
})
}
/**
* @param {Ty.Event.Outer<Ty.Event.M_Room_Message>} event
* @param {import("discord-api-types/v10").APIGuild} guild
* @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API
*/
function eventToMessage(event) {
async function eventToMessage(event, guild, di) {
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */
let messages = []
let displayName = event.sender
let avatarURL = undefined
let replyLine = ""
// Extract a basic display name from the sender
const match = event.sender.match(/^@(.*?):/)
if (match) {
displayName = match[1]
// TODO: get the media repo domain and the avatar url from the matrix member event
}
if (match) displayName = match[1]
// Try to extract an accurate display name and avatar URL from the member event
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
if (member.displayname) displayName = member.displayname
if (member.avatar_url) avatarURL = utils.getPublicUrlForMxc(member.avatar_url)
// Convert content depending on what the message is
let content = event.content.body // ultimate fallback
@ -61,9 +120,39 @@ function eventToMessage(event) {
// input = input.replace(/ /g, "&nbsp;")
// There is also a corresponding test to uncomment, named "event2message: whitespace is retained"
// Handling replies. We'll look up the data of the replied-to event from the Matrix homeserver.
await (async () => {
const repliedToEventId = event.content["m.relates_to"]?.["m.in_reply_to"].event_id
if (!repliedToEventId) return
const repliedToEvent = await di.api.getEvent(event.room_id, repliedToEventId)
if (!repliedToEvent) return
const row = db.prepare("SELECT channel_id, message_id FROM event_message WHERE event_id = ? ORDER BY part").get(repliedToEventId)
if (row) {
replyLine = `<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/${guild.id}/${row.channel_id}/${row.message_id} `
} else {
replyLine = `<:L1:1144820033948762203><:L2:1144820084079087647>`
}
const sender = repliedToEvent.sender
const senderName = sender.match(/@([^:]*)/)?.[1] || sender
const authorID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(repliedToEvent.sender)
if (authorID) {
replyLine += `<@${authorID}>: `
} else {
replyLine += `Ⓜ️**${senderName}**: `
}
const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body
const contentPreviewChunks = chunk(repliedToContent.replace(/.*<\/mx-reply>/, "").replace(/(?:\n|<br>)+/g, " ").replace(/<[^>]+>/g, ""), 24)
const contentPreview = contentPreviewChunks.length > 1 ? contentPreviewChunks[0] + "..." : contentPreviewChunks[0]
replyLine += contentPreview + "\n"
})()
// Element adds a bunch of <br> before </blockquote> but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those.
input = input.replace(/(?:\n|<br ?\/?>\s*)*<\/blockquote>/g, "</blockquote>")
// The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason.
// But I should not count it if it's between block elements.
input = input.replace(/(<\/?([^ >]+)[^>]*>)?\n(<\/?([^ >]+)[^>]*>)?/g, (whole, beforeContext, beforeTag, afterContext, afterTag) => {
// console.error(beforeContext, beforeTag, afterContext, afterTag)
if (typeof beforeTag !== "string" && typeof afterTag !== "string") {
return "<br>"
}
@ -89,6 +178,8 @@ function eventToMessage(event) {
content = content.replace(/([*_~`#])/g, `\\$1`)
}
content = replyLine + content
// Split into 2000 character chunks
const chunks = chunk(content, 2000)
messages = messages.concat(chunks.map(content => ({

View file

@ -1,18 +1,42 @@
// @ts-check
const {test} = require("supertape")
const {eventToMessage} = require("./event-to-message")
const data = require("../../test/data")
/**
* @param {string} roomID
* @param {string} eventID
* @returns {(roomID: string, eventID: string) => Promise<Ty.Event.Outer<Ty.Event.M_Room_Message>>}
*/
function mockGetEvent(t, roomID_in, eventID_in, outer) {
return async function(roomID, eventID) {
t.equal(roomID, roomID_in)
t.equal(eventID, eventID_in)
return new Promise(resolve => {
setTimeout(() => {
resolve({
event_id: eventID_in,
room_id: roomID_in,
origin_server_ts: 1680000000000,
unsigned: {
age: 2245,
transaction_id: "$local.whatever"
},
...outer
})
})
})
}
}
function sameFirstContentAndWhitespace(t, a, b) {
const a2 = JSON.stringify(a[0].content)
const b2 = JSON.stringify(b[0].content)
t.equal(a2, b2)
}
test("event2message: body is used when there is no formatted_body", t => {
test("event2message: body is used when there is no formatted_body", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
body: "testing plaintext",
msgtype: "m.text"
@ -27,16 +51,16 @@ test("event2message: body is used when there is no formatted_body", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "testing plaintext",
avatar_url: undefined
}]
)
})
test("event2message: any markdown in body is escaped", t => {
test("event2message: any markdown in body is escaped", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
body: "testing **special** ~~things~~ which _should_ *not* `trigger` @any <effects>",
msgtype: "m.text"
@ -51,16 +75,16 @@ test("event2message: any markdown in body is escaped", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "testing \\*\\*special\\*\\* \\~\\~things\\~\\~ which \\_should\\_ \\*not\\* \\`trigger\\` @any <effects>",
avatar_url: undefined
}]
)
})
test("event2message: basic html is converted to markdown", t => {
test("event2message: basic html is converted to markdown", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
@ -77,16 +101,16 @@ test("event2message: basic html is converted to markdown", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "this **is** a **_test_** of ~~formatting~~",
avatar_url: undefined
}]
)
})
test("event2message: markdown syntax is escaped", t => {
test("event2message: markdown syntax is escaped", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
@ -103,16 +127,16 @@ test("event2message: markdown syntax is escaped", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "this \\*\\*is\\*\\* an **_extreme_** \\\\\\*test\\\\\\* of",
avatar_url: undefined
}]
)
})
test("event2message: html lines are bridged correctly", t => {
test("event2message: html lines are bridged correctly", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
@ -129,16 +153,16 @@ test("event2message: html lines are bridged correctly", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "paragraph one\nline _two_\nline three\n\nparagraph two\nline _two_\nline three\n\nparagraph three\n\nparagraph four\nline two\nline three\nline four\n\nparagraph five",
avatar_url: undefined
}]
)
})
/*test("event2message: whitespace is retained", t => {
/*test("event2message: whitespace is retained", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
@ -155,17 +179,17 @@ test("event2message: html lines are bridged correctly", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\n line five",
avatar_url: undefined
}]
)
})*/
test("event2message: whitespace is collapsed", t => {
test("event2message: whitespace is collapsed", async t => {
sameFirstContentAndWhitespace(
t,
eventToMessage({
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
@ -182,17 +206,17 @@ test("event2message: whitespace is collapsed", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\nline five",
avatar_url: undefined
}]
)
})
test("event2message: lists are bridged correctly", t => {
test("event2message: lists are bridged correctly", async t => {
sameFirstContentAndWhitespace(
t,
eventToMessage({
await eventToMessage({
"type": "m.room.message",
"sender": "@cadence:cadence.moe",
"content": {
@ -210,16 +234,16 @@ test("event2message: lists are bridged correctly", t => {
"room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe"
}),
[{
username: "cadence",
username: "cadence [they]",
content: "* line one\n* line two\n* line three\n * nested one\n * nested two\n* line four",
avatar_url: undefined
}]
)
})
test("event2message: long messages are split", t => {
test("event2message: long messages are split", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
body: ("a".repeat(130) + " ").repeat(19),
msgtype: "m.text"
@ -234,20 +258,20 @@ test("event2message: long messages are split", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: (("a".repeat(130) + " ").repeat(15)).slice(0, -1),
avatar_url: undefined
}, {
username: "cadence",
username: "cadence [they]",
content: (("a".repeat(130) + " ").repeat(4)).slice(0, -1),
avatar_url: undefined
}]
)
})
test("event2message: code blocks work", t => {
test("event2message: code blocks work", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
@ -264,17 +288,69 @@ test("event2message: code blocks work", t => {
}
}),
[{
username: "cadence",
username: "cadence [they]",
content: "preceding\n\n```\ncode block\n```\n\nfollowing `code` is inline",
avatar_url: undefined
}]
)
})
test("event2message: m.emote markdown syntax is escaped", t => {
test("event2message: code block contents are formatted correctly and not escaped", async t => {
t.deepEqual(
eventToMessage({
await eventToMessage({
"type": "m.room.message",
"sender": "@cadence:cadence.moe",
"content": {
"msgtype": "m.text",
"body": "wrong body",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code>input = input.replace(/(&lt;\\/?([^ &gt;]+)[^&gt;]*&gt;)?\\n(&lt;\\/?([^ &gt;]+)[^&gt;]*&gt;)?/g,\n_input_ = input = input.replace(/(&lt;\\/?([^ &gt;]+)[^&gt;]*&gt;)?\\n(&lt;\\/?([^ &gt;]+)[^&gt;]*&gt;)?/g,\n</code></pre>\n<p><code>input = input.replace(/(&lt;\\/?([^ &gt;]+)[^&gt;]*&gt;)?\\n(&lt;\\/?([^ &gt;]+)[^&gt;]*&gt;)?/g,</code></p>\n"
},
"origin_server_ts": 1693031482275,
"unsigned": {
"age": 99,
"transaction_id": "m1693031482146.511"
},
"event_id": "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s",
"room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe"
}),
[{
username: "cadence [they]",
content: "```\ninput = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n_input_ = input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n```\n\n`input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,`",
avatar_url: undefined
}]
)
})
test("event2message: quotes have an appropriate amount of whitespace", async t => {
t.deepEqual(
await eventToMessage({
content: {
msgtype: "m.text",
body: "wrong body",
format: "org.matrix.custom.html",
formatted_body: "<blockquote>Chancellor of Germany Angela Merkel, on March 17, 2017: they did not shake hands<br><br><br></blockquote><br>🤨"
},
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
origin_server_ts: 1688301929913,
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
sender: "@cadence:cadence.moe",
type: "m.room.message",
unsigned: {
age: 405299
}
}),
[{
username: "cadence [they]",
content: "> Chancellor of Germany Angela Merkel, on March 17, 2017: they did not shake hands\n🤨",
avatar_url: undefined
}]
)
})
test("event2message: m.emote markdown syntax is escaped", async t => {
t.deepEqual(
await eventToMessage({
content: {
msgtype: "m.emote",
body: "wrong body",
@ -291,9 +367,142 @@ test("event2message: m.emote markdown syntax is escaped", t => {
}
}),
[{
username: "cadence",
content: "\\* cadence shows you \\*\\*her\\*\\* **_extreme_** \\\\\\*test\\\\\\* of",
username: "cadence [they]",
content: "\\* cadence \\[they\\] shows you \\*\\*her\\*\\* **_extreme_** \\\\\\*test\\\\\\* of",
avatar_url: undefined
}]
)
})
test("event2message: rich reply to a sim user", async t => {
t.deepEqual(
await eventToMessage({
"type": "m.room.message",
"sender": "@cadence:cadence.moe",
"content": {
"msgtype": "m.text",
"body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04?via=cadence.moe&via=feather.onl\">In reply to</a> <a href=\"https://matrix.to/#/@_ooye_kyuugryphon:cadence.moe\">@_ooye_kyuugryphon:cadence.moe</a><br>Slow news day.</blockquote></mx-reply>Testing this reply, ignore",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
}
}
},
"origin_server_ts": 1693029683016,
"unsigned": {
"age": 91,
"transaction_id": "m1693029682894.510"
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
}, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
type: "m.room.message",
content: {
msgtype: "m.text",
body: "Slow news day."
},
sender: "@_ooye_kyuugryphon:cadence.moe"
})
}
}),
[{
username: "cadence [they]",
content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>: Slow news day.\nTesting this reply, ignore",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU"
}]
)
})
test("event2message: rich reply to a matrix user's long message with formatting", async t => {
t.deepEqual(
await eventToMessage({
"type": "m.room.message",
"sender": "@cadence:cadence.moe",
"content": {
"msgtype": "m.text",
"body": "> <@cadence:cadence.moe> ```\n> i should have a little happy test\n> ```\n> * list **bold** _em_ ~~strike~~\n> # heading 1\n> ## heading 2\n> ### heading 3\n> https://cadence.moe\n> [legit website](https://cadence.moe)\n\nno you can't!!!",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04?via=cadence.moe&via=feather.onl\">In reply to</a> <a href=\"https://matrix.to/#/@cadence:cadence.moe\">@cadence:cadence.moe</a><br><pre><code>i should have a little happy test\n</code></pre>\n<ul>\n<li>list <strong>bold</strong> <em>em</em> ~~strike~~</li>\n</ul>\n<h1>heading 1</h1>\n<h2>heading 2</h2>\n<h3>heading 3</h3>\n<p>https://cadence.moe<br /><a href=\"https://cadence.moe\">legit website</a></p>\n</blockquote></mx-reply><strong>no you can't!!!</strong>",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
}
}
},
"origin_server_ts": 1693037401693,
"unsigned": {
"age": 381,
"transaction_id": "m1693037401592.521"
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
}, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
"type": "m.room.message",
"sender": "@cadence:cadence.moe",
"content": {
"msgtype": "m.text",
"body": "```\ni should have a little happy test\n```\n* list **bold** _em_ ~~strike~~\n# heading 1\n## heading 2\n### heading 3\nhttps://cadence.moe\n[legit website](https://cadence.moe)",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code>i should have a little happy test\n</code></pre>\n<ul>\n<li>list <strong>bold</strong> <em>em</em> ~~strike~~</li>\n</ul>\n<h1>heading 1</h1>\n<h2>heading 2</h2>\n<h3>heading 3</h3>\n<p>https://cadence.moe<br><a href=\"https://cadence.moe\">legit website</a></p>\n"
}
})
}
}),
[{
username: "cadence [they]",
content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: i should have a little...\n**no you can't!!!**",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU"
}]
)
})
test("event2message: with layered rich replies, the preview should only be the real text", async t => {
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "> <@cadence:cadence.moe> two\n\nthree",
format: "org.matrix.custom.html",
formatted_body: "<mx-reply><blockquote><a href=\"https://matrix.to/#/!PnyBKvUBOhjuCucEfk:cadence.moe/$f-noT-d-Eo_Xgpc05Ww89ErUXku4NwKWYGHLzWKo1kU?via=cadence.moe\">In reply to</a> <a href=\"https://matrix.to/#/@cadence:cadence.moe\">@cadence:cadence.moe</a><br>two</blockquote></mx-reply>three",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
}
}
},
event_id: "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
}, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
"type": "m.room.message",
"sender": "@cadence:cadence.moe",
"content": {
"msgtype": "m.text",
"body": "> <@cadence:cadence.moe> one\n\ntwo",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!PnyBKvUBOhjuCucEfk:cadence.moe/$5UtboIC30EFlAYD_Oh0pSYVW8JqOp6GsDIJZHtT0Wls?via=cadence.moe\">In reply to</a> <a href=\"https://matrix.to/#/@cadence:cadence.moe\">@cadence:cadence.moe</a><br>one</blockquote></mx-reply>two",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$5UtboIC30EFlAYD_Oh0pSYVW8JqOp6GsDIJZHtT0Wls"
}
}
}
})
}
}),
[{
username: "cadence [they]",
content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: two\nthree",
avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU"
}]
)
})

View file

@ -19,4 +19,15 @@ function eventSenderIsFromDiscord(sender) {
return false
}
/**
* @param {string} mxc
* @returns {string?}
*/
function getPublicUrlForMxc(mxc) {
const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (avatarURLParts) return `https://matrix.cadence.moe/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}`
else return null
}
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
module.exports.getPublicUrlForMxc = getPublicUrlForMxc

View file

@ -80,3 +80,24 @@ async event => {
const url = event.content.url || null
db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE room_id = ?").run(url, event.room_id)
}))
sync.addTemporaryListener(as, "type:m.room.name", guard("m.room.name",
/**
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Name>} event
*/
async event => {
if (event.state_key !== "") return
if (utils.eventSenderIsFromDiscord(event.sender)) return
const name = event.content.name || null
db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id)
}))
sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
/**
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Member>} event
*/
async event => {
if (event.state_key[0] !== "@") return
if (utils.eventSenderIsFromDiscord(event.sender)) return
db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(event.room_id, event.sender, event.content.displayname || null, event.content.avatar_url || null)
}))

13
types.d.ts vendored
View file

@ -70,12 +70,17 @@ export namespace Event {
msgtype: "m.text" | "m.emote"
body: string
format?: "org.matrix.custom.html"
formatted_body?: string
formatted_body?: string,
"m.relates_to"?: {
"m.in_reply_to": {
event_id: string
}
}
}
export type M_Room_Member = {
membership: string
display_name?: string
displayname?: string
avatar_url?: string
}
@ -84,6 +89,10 @@ export namespace Event {
url?: string
}
export type M_Room_Name = {
name?: string
}
export type M_Reaction = {
"m.relates_to": {
rel_type: "m.annotation"