Compare commits

..

4 commits

Author SHA1 Message Date
8026cf0cad Coverage for m->d ordered list start attribute 2023-11-23 13:41:31 +13:00
53a009ca45 m->d: Users who aren't joined can be mentioned
This works by writing @name in the message, where `name` is the username
or displayname of the person in the guild you want to mention. If it
matched, the person will be joined and mentioned on their side.

Unfortunately this requires you to guess the person's name, and may lead
to embarrassment if it doesn't activate as you intended. Good luck!
2023-11-23 13:41:02 +13:00
39528b0557 Refactor attachmentToEvent to a function 2023-11-23 13:37:09 +13:00
5247a5d70e Update remarks in readme 2023-11-23 13:36:40 +13:00
5 changed files with 264 additions and 98 deletions

View file

@ -86,6 +86,103 @@ const embedTitleParser = markdown.markdownEngine.parserFor({
link: undefined link: undefined
}) })
/**
* @param {{room?: boolean, user_ids?: string[]}} mentions
* @param {DiscordTypes.APIAttachment} attachment
*/
async function attachmentToEvent(mentions, attachment) {
const emoji =
attachment.content_type?.startsWith("image/jp") ? "📸"
: attachment.content_type?.startsWith("image/") ? "🖼️"
: attachment.content_type?.startsWith("video/") ? "🎞️"
: attachment.content_type?.startsWith("text/") ? "📝"
: attachment.content_type?.startsWith("audio/") ? "🎶"
: "📄"
// no native media spoilers in Element, so we'll post a link instead, forcing it to not preview using a blockquote
if (attachment.filename.startsWith("SPOILER_")) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.text",
body: `${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})`,
format: "org.matrix.custom.html",
formatted_body: `<blockquote>${emoji} Uploaded SPOILER file: <a href="${attachment.url}"><span data-mx-spoiler>${attachment.url}</span></a> (${pb(attachment.size)})</blockquote>`
}
}
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
else if (attachment.size > reg.ooye.max_file_size) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.text",
body: `${emoji} Uploaded file: ${attachment.url} (${pb(attachment.size)})`,
format: "org.matrix.custom.html",
formatted_body: `${emoji} Uploaded file: <a href="${attachment.url}">${attachment.filename}</a> (${pb(attachment.size)})`
}
} else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.image",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
w: attachment.width,
h: attachment.height,
size: attachment.size
}
}
} else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.video",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.description || attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
w: attachment.width,
h: attachment.height,
size: attachment.size
}
}
} else if (attachment.content_type?.startsWith("audio/")) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.audio",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.description || attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
size: attachment.size,
duration: attachment.duration_secs ? attachment.duration_secs * 1000 : undefined
}
}
} else {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.file",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
size: attachment.size
}
}
}
}
/** /**
* @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
@ -316,98 +413,7 @@ async function messageToEvent(message, guild, options = {}, di) {
} }
// Then attachments // Then attachments
const attachmentEvents = await Promise.all(message.attachments.map(async attachment => { const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions)))
const emoji =
attachment.content_type?.startsWith("image/jp") ? "📸"
: attachment.content_type?.startsWith("image/") ? "🖼️"
: attachment.content_type?.startsWith("video/") ? "🎞️"
: attachment.content_type?.startsWith("text/") ? "📝"
: attachment.content_type?.startsWith("audio/") ? "🎶"
: "📄"
// no native media spoilers in Element, so we'll post a link instead, forcing it to not preview using a blockquote
if (attachment.filename.startsWith("SPOILER_")) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.text",
body: `${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})`,
format: "org.matrix.custom.html",
formatted_body: `<blockquote>${emoji} Uploaded SPOILER file: <a href="${attachment.url}"><span data-mx-spoiler>${attachment.url}</span></a> (${pb(attachment.size)})</blockquote>`
}
}
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
else if (attachment.size > reg.ooye.max_file_size) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.text",
body: `${emoji} Uploaded file: ${attachment.url} (${pb(attachment.size)})`,
format: "org.matrix.custom.html",
formatted_body: `${emoji} Uploaded file: <a href="${attachment.url}">${attachment.filename}</a> (${pb(attachment.size)})`
}
} else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.image",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
w: attachment.width,
h: attachment.height,
size: attachment.size
}
}
} else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.video",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.description || attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
w: attachment.width,
h: attachment.height,
size: attachment.size
}
}
} else if (attachment.content_type?.startsWith("audio/")) {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.audio",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.description || attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
size: attachment.size,
duration: attachment.duration_secs ? attachment.duration_secs * 1000 : undefined
}
}
} else {
return {
$type: "m.room.message",
"m.mentions": mentions,
msgtype: "m.file",
url: await file.uploadDiscordFileToMxc(attachment.url),
external_url: attachment.url,
body: attachment.filename,
filename: attachment.filename,
info: {
mimetype: attachment.content_type,
size: attachment.size
}
}
}
}))
events.push(...attachmentEvents) events.push(...attachmentEvents)
// Then embeds // Then embeds

View file

@ -73,7 +73,7 @@ async function sendEvent(event) {
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
let {messagesToEdit, messagesToSend, messagesToDelete} = await eventToMessage.eventToMessage(event, guild, {api}) let {messagesToEdit, messagesToSend, messagesToDelete} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow})
messagesToEdit = await Promise.all(messagesToEdit.map(async e => { messagesToEdit = await Promise.all(messagesToEdit.map(async e => {
e.message = await resolvePendingFiles(e.message) e.message = await resolvePendingFiles(e.message)

View file

@ -122,7 +122,7 @@ turndownService.addRule("listItem", {
if (parent.nodeName === "OL") { if (parent.nodeName === "OL") {
var start = parent.getAttribute("start") var start = parent.getAttribute("start")
var index = Array.prototype.indexOf.call(parent.children, node) var index = Array.prototype.indexOf.call(parent.children, node)
prefix = (start ? Number(start) + index : index + 1) + ". " prefix = (start ? Number(start) + index : index + 1) + ". "
} }
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? "\n" : "") return prefix + content + (node.nextSibling && !/\n$/.test(content) ? "\n" : "")
} }
@ -259,7 +259,7 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles)
/** /**
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event
* @param {import("discord-api-types/v10").APIGuild} guild * @param {import("discord-api-types/v10").APIGuild} guild
* @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer}} di simple-as-nails dependency injection for the matrix API
*/ */
async function eventToMessage(event, guild, di) { async function eventToMessage(event, guild, di) {
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */ /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */
@ -289,6 +289,8 @@ async function eventToMessage(event, guild, di) {
const attachments = [] const attachments = []
/** @type {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */ /** @type {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} */
const pendingFiles = [] const pendingFiles = []
/** @type {DiscordTypes.APIUser[]} */
const ensureJoined = []
// Convert content depending on what the message is // Convert content depending on what the message is
if (event.type === "m.room.message" && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote")) { if (event.type === "m.room.message" && (event.content.msgtype === "m.text" || event.content.msgtype === "m.emote")) {
@ -502,6 +504,17 @@ async function eventToMessage(event, guild, di) {
content = displayNameRunoff + replyLine + content content = displayNameRunoff + replyLine + content
// Handling written @mentions: we need to look for candidate Discord members to join to the room
let writtenMentionMatch = content.match(/(?:^|[^"<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // d flag requires Node 16+
if (writtenMentionMatch) {
const results = await di.snow.guild.searchGuildMembers(guild.id, {query: writtenMentionMatch[1]})
if (results[0]) {
assert(results[0].user)
content = content.slice(0, writtenMentionMatch.index) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.index + writtenMentionMatch[0].length)
ensureJoined.push(results[0].user)
}
}
// Split into 2000 character chunks // Split into 2000 character chunks
const chunks = chunk(content, 2000) const chunks = chunk(content, 2000)
messages = messages.concat(chunks.map(content => ({ messages = messages.concat(chunks.map(content => ({
@ -543,7 +556,8 @@ async function eventToMessage(event, guild, di) {
return { return {
messagesToEdit, messagesToEdit,
messagesToSend, messagesToSend,
messagesToDelete: messageIDsToEdit messagesToDelete: messageIDsToEdit,
ensureJoined
} }
} }

View file

@ -63,6 +63,7 @@ test("event2message: body is used when there is no formatted_body", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -89,8 +90,15 @@ test("event2message: any markdown in body is escaped, except strikethrough", asy
unsigned: { unsigned: {
age: 405299 age: 405299
} }
}, {}, {
snow: {
guild: {
searchGuildMembers: () => []
}
}
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -122,6 +130,7 @@ test("event2message: links in formatted body are not broken", async t => {
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -151,6 +160,7 @@ test("event2message: links in plaintext body are not broken", async t => {
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -181,6 +191,7 @@ test("event2message: basic html is converted to markdown", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -211,6 +222,7 @@ test("event2message: spoilers work", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -241,6 +253,7 @@ test("event2message: markdown syntax is escaped", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -271,6 +284,7 @@ test("event2message: html lines are bridged correctly", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -301,6 +315,7 @@ test("event2message: html lines are bridged correctly", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -332,6 +347,7 @@ test("event2message: whitespace is collapsed", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -364,6 +380,7 @@ test("event2message: lists are bridged correctly", async t => {
"room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -392,6 +409,7 @@ test("event2message: long messages are split", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -426,6 +444,7 @@ test("event2message: code blocks work", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -457,6 +476,7 @@ test("event2message: code block contents are formatted correctly and not escaped
"room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -487,6 +507,7 @@ test("event2message: quotes have an appropriate amount of whitespace", async t =
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -528,6 +549,7 @@ test("event2message: lists have appropriate line breaks", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -539,6 +561,48 @@ test("event2message: lists have appropriate line breaks", async t => {
) )
}) })
test("event2message: ordered list start attribute works", async t => {
t.deepEqual(
await eventToMessage({
content: {
body: 'i am not certain what you mean by "already exists with as discord". my goals are\n' +
'1. bridgeing specific channels with existing matrix rooms\n' +
' 2. optionally maybe entire "servers"\n' +
'3. offering the bridge as a public service ',
format: 'org.matrix.custom.html',
formatted_body: '<p>i am not certain what you mean by "already exists with as discord". my goals are</p>\n' +
'<ol>\n' +
'<li>bridgeing specific channels with existing matrix rooms\n' +
'<ol start="2">\n' +
'<li>optionally maybe entire "servers"</li>\n' +
'</ol>\n' +
'</li>\n' +
'<li>offering the bridge as a public service</li>\n' +
'</ol>\n',
'm.mentions': {},
msgtype: 'm.text'
},
room_id: '!cBxtVRxDlZvSVhJXVK:cadence.moe',
sender: '@Milan:tchncs.de',
type: 'm.room.message',
}, {}, {
api: {
getStateEvent: async () => ({displayname: "Milan"})
}
}),
{
ensureJoined: [],
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "Milan",
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`,
avatar_url: undefined
}]
}
)
})
test("event2message: m.emote plaintext works", async t => { test("event2message: m.emote plaintext works", async t => {
t.deepEqual( t.deepEqual(
await eventToMessage({ await eventToMessage({
@ -556,6 +620,7 @@ test("event2message: m.emote plaintext works", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -586,6 +651,7 @@ test("event2message: m.emote markdown syntax is escaped", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -633,6 +699,7 @@ test("event2message: rich reply to a sim user", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -709,6 +776,7 @@ test("event2message: rich reply to an already-edited message will quote the new
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -756,6 +824,7 @@ test("event2message: should avoid using blockquote contents as reply preview in
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -841,6 +910,7 @@ test("event2message: should include a reply preview when message ends with a blo
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -921,6 +991,7 @@ test("event2message: should include a reply preview when replying to a descripti
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -970,6 +1041,7 @@ test("event2message: entities are not escaped in main message or reply preview",
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1048,6 +1120,7 @@ test("event2message: editing a rich reply to a sim user", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [{ messagesToEdit: [{
id: "1144874214311067708", id: "1144874214311067708",
@ -1102,6 +1175,7 @@ test("event2message: editing a plaintext body message", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [{ messagesToEdit: [{
id: "1145688633186193479", id: "1145688633186193479",
@ -1153,6 +1227,7 @@ test("event2message: editing a plaintext message to be longer", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [{ messagesToEdit: [{
id: "1145688633186193479", id: "1145688633186193479",
@ -1208,6 +1283,7 @@ test("event2message: editing a plaintext message to be shorter", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: ["1145688633186193481"], messagesToDelete: ["1145688633186193481"],
messagesToEdit: [{ messagesToEdit: [{
id: "1145688633186193480", id: "1145688633186193480",
@ -1265,6 +1341,7 @@ test("event2message: editing a formatted body message", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [{ messagesToEdit: [{
id: "1145688633186193479", id: "1145688633186193479",
@ -1317,6 +1394,7 @@ test("event2message: rich reply to a matrix user's long message with formatting"
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1376,6 +1454,7 @@ test("event2message: rich reply to an image", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1427,6 +1506,7 @@ test("event2message: rich reply to a spoiler should ensure the spoiler is hidden
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1478,6 +1558,7 @@ test("event2message: with layered rich replies, the preview should only be the r
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1508,6 +1589,7 @@ test("event2message: raw mentioning discord users in plaintext body works", asyn
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1538,6 +1620,7 @@ test("event2message: raw mentioning discord users in formatted body works", asyn
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1568,6 +1651,7 @@ test("event2message: mentioning discord users works", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1598,6 +1682,7 @@ test("event2message: mentioning matrix users works", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1628,6 +1713,7 @@ test("event2message: mentioning bridged rooms works", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1658,6 +1744,7 @@ test("event2message: colon after mentions is stripped", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1699,6 +1786,7 @@ test("event2message: caches the member if the member is not known", async t => {
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1743,6 +1831,7 @@ test("event2message: skips caching the member if the member does not exist, some
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1786,6 +1875,7 @@ test("event2message: overly long usernames are shifted into the message content"
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1818,6 +1908,7 @@ test("event2message: overly long usernames are not treated specially when the ms
} }
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1847,6 +1938,7 @@ test("event2message: text attachments work", async t => {
room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1881,6 +1973,7 @@ test("event2message: image attachments work", async t => {
room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1930,6 +2023,7 @@ test("event2message: encrypted image attachments work", async t => {
room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -1974,6 +2068,7 @@ test("event2message: stickers work", async t => {
room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe" room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -2002,6 +2097,7 @@ test("event2message: static emojis work", async t => {
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -2028,6 +2124,7 @@ test("event2message: animated emojis work", async t => {
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -2054,6 +2151,7 @@ test("event2message: unknown emojis in the middle are linked", async t => {
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}), }),
{ {
ensureJoined: [],
messagesToDelete: [], messagesToDelete: [],
messagesToEdit: [], messagesToEdit: [],
messagesToSend: [{ messagesToSend: [{
@ -2065,6 +2163,53 @@ test("event2message: unknown emojis in the middle are linked", async t => {
) )
}) })
test("event2message: guessed @mentions may join members to mention", async t => {
let called = 0
const subtext = {
user: {
id: "321876634777218072",
username: "subtext",
discriminator: "0"
}
}
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "@subtext: what food would you like to order?"
},
event_id: "$u5gSwSzv_ZQS3eM00mnTBCor8nx_A_AwuQz7e59PZk8",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {
id: "112760669178241024"
}, {
snow: {
guild: {
async searchGuildMembers(guildID, options) {
called++
t.equal(guildID, "112760669178241024")
t.deepEqual(options, {query: "subtext"})
return [subtext]
}
}
}
}),
{
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
content: "<@321876634777218072> what food would you like to order?",
avatar_url: undefined
}],
ensureJoined: [subtext.user]
}
)
t.equal(called, 1, "searchGuildMembers should be called once")
})
slow()("event2message: unknown emoji in the end is reuploaded as a sprite sheet", async t => { slow()("event2message: unknown emoji in the end is reuploaded as a sprite sheet", async t => {
const messages = await eventToMessage({ const messages = await eventToMessage({
type: "m.room.message", type: "m.room.message",

View file

@ -15,7 +15,7 @@ This readme has the most important info. The rest is [in the docs folder.](https
* Modern: Supports new Discord features like replies, threads and stickers, and new Matrix features like edits, spaces and space membership. * Modern: Supports new Discord features like replies, threads and stickers, and new Matrix features like edits, spaces and space membership.
* Efficient: Special attention has been given to memory usage, database indexes, disk footprint, runtime algorithms, and queries to the homeserver. * Efficient: Special attention has been given to memory usage, database indexes, disk footprint, runtime algorithms, and queries to the homeserver.
* Reliable: Any errors on either side are notified on Matrix and can be retried. * Reliable: Any errors on either side are notified on Matrix and can be retried.
* Tested: A test suite and code coverage make sure all the core logic works. * Tested: A test suite and code coverage make sure all the logic and special cases work.
* Simple development: No build step (it's JavaScript, not TypeScript), minimal/lightweight dependencies, and abstraction only where necessary so that less background knowledge is required. No need to learn about Intents or library functions. * Simple development: No build step (it's JavaScript, not TypeScript), minimal/lightweight dependencies, and abstraction only where necessary so that less background knowledge is required. No need to learn about Intents or library functions.
* No locking algorithm: Other bridges use a locking algorithm which is a source of frequent bugs. This bridge avoids the need for one. * No locking algorithm: Other bridges use a locking algorithm which is a source of frequent bugs. This bridge avoids the need for one.
* Latest API: Being on the latest Discord API version lets it access all features, without the risk of deprecated API versions being removed. * Latest API: Being on the latest Discord API version lets it access all features, without the risk of deprecated API versions being removed.
@ -42,13 +42,14 @@ Most features you'd expect in both directions, plus a little extra spice:
* Custom emojis in messages * Custom emojis in messages
* Custom room names/avatars can be applied on Matrix-side * Custom room names/avatars can be applied on Matrix-side
* Larger files from Discord are linked instead of reuploaded to Matrix * Larger files from Discord are linked instead of reuploaded to Matrix
* Simulated user accounts are named @the_persons_username rather than @112233445566778899
For more information about features, [see the user guide.](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/user-guide.md) For more information about features, [see the user guide.](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/user-guide.md)
## Caveats ## Caveats
* This bridge is not designed for puppetting. * This bridge is not designed for puppetting.
* Direct Messaging is not supported yet. * Direct Messaging is not supported until I figure out a good way of doing it.
## Efficiency details ## Efficiency details