Add tests for emoji sheet; style and nits

This commit is contained in:
Cadence Ember
2026-02-11 02:40:18 +13:00
parent c0d82754b0
commit d1b0fa48cf
9 changed files with 280 additions and 224 deletions
-1
View File
@@ -66,7 +66,6 @@
"setup": "node --enable-source-maps scripts/setup.js", "setup": "node --enable-source-maps scripts/setup.js",
"addbot": "node addbot.js", "addbot": "node addbot.js",
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
"cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/m2d/event-dispatcher.js -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" "cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/m2d/event-dispatcher.js -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
} }
} }
+4
View File
@@ -34,5 +34,9 @@ passthrough.select = orm.select
console.log("Discord gateway started") console.log("Discord gateway started")
sync.require("../src/web/server") sync.require("../src/web/server")
discord.cloud.once("ready", () => {
as.listen()
})
require("../src/stdin") require("../src/stdin")
})() })()
+19 -26
View File
@@ -348,10 +348,11 @@ function getUserOrProxyOwnerMention(mxid) {
/** /**
* At the time of this executing, we know what the end of message emojis are, and we know that at least one of them is unknown. * At the time of this executing, we know what the end of message emojis are, and we know that at least one of them is unknown.
* This function will strip them from the content and generate a link in it's place for proxying a sprite sheet that discord previews. * This function will strip them from the content and add a link that Discord will preview with a sprite sheet of emojis.
* @param {string} content * @param {string} content
* @returns {string} new content with emoji sheet link
*/ */
async function uploadEndOfMessageSpriteSheet(content) { function linkEndOfMessageSpriteSheet(content) {
if (!content.includes("<::>")) return content // No unknown emojis, nothing to do if (!content.includes("<::>")) return content // No unknown emojis, nothing to do
// Remove known and unknown emojis from the end of the message // Remove known and unknown emojis from the end of the message
const r = /<a?:[a-zA-Z0-9_]*:[0-9]*>\s*$/ const r = /<a?:[a-zA-Z0-9_]*:[0-9]*>\s*$/
@@ -360,33 +361,25 @@ async function uploadEndOfMessageSpriteSheet(content) {
content = content.replace(r, "") content = content.replace(r, "")
} }
let emojiSheetUrl = new URL('emoji/matrix', reg.ooye.bridge_origin) // Use a markdown link to hide the URL. If this is the only thing in the message, Discord will hide it entirely, same as lone URLs. Good for us.
for(let mxc of endOfMessageEmojis) { content = content.trimEnd()
const emojiSheetUrlString = emojiSheetUrl.toString() content += " [\u2800](" // U+2800 Braille Pattern Blank is invisible on all known platforms but is digitally not a whitespace character
const afterLink = ")"
// Discord will not preview the URL if it is longer than 2000 characters. // Make emojis URL params
// Longer URLs can preview but it's very inconsistant so ignoring any additional emojis const params = new URLSearchParams()
if(emojiSheetUrlString.length + mxc.length > 2000) { for (const mxc of endOfMessageEmojis) {
// We can do up to 2000 chars max. (In this maximal case it will get chunked to a separate message.) Ignore additional emojis.
const withoutMxc = mxUtils.makeMxcPublic(mxc)
const emojisLength = params.toString().length + encodeURIComponent(withoutMxc).length + 2
if (content.length + emojisLength + afterLink.length > 2000) {
break break
} }
params.append("e", withoutMxc)
// Ignoring any additional emojis if it would exceed discords message length
if(emojiSheetUrlString.length + mxc.length + content.length > 4000) {
break
}
mxUtils.generatePermittedMediaHash(mxc)
emojiSheetUrl.searchParams.append('e', mxc)
} }
const emojiSheetUrlString = emojiSheetUrl.toString() const url = `${reg.ooye.bridge_origin}/download/sheet?${params.toString()}`
return content + url + afterLink
// Discord message length check. Using markdown link with placeholder text since discord displays
// the link when there is preceeding text to the emoji
const placeholderText = '.'
content = content.concat(`[${placeholderText}](${emojiSheetUrlString})`)
return content
} }
/** /**
@@ -543,7 +536,7 @@ async function getL1L2ReplyLine(called = false) {
* @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 | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_End} 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 | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start | Ty.Event.Outer_Org_Matrix_Msc3381_Poll_End} event
* @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuild} guild
* @param {DiscordTypes.APIGuildTextChannel} channel * @param {DiscordTypes.APIGuildTextChannel} channel
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, mxcDownloader: (mxc: string) => Promise<Buffer | undefined>, pollEnd?: {messageID: string}}} di simple-as-nails dependency injection for the matrix API * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, pollEnd?: {messageID: string}}} di simple-as-nails dependency injection for the matrix API
*/ */
async function eventToMessage(event, guild, channel, di) { async function eventToMessage(event, guild, channel, di) {
let displayName = event.sender let displayName = event.sender
@@ -956,7 +949,7 @@ async function eventToMessage(event, guild, channel, di) {
if (replyLine && content.startsWith("> ")) content = "\n" + content if (replyLine && content.startsWith("> ")) content = "\n" + content
// SPRITE SHEET EMOJIS FEATURE: // SPRITE SHEET EMOJIS FEATURE:
content = await uploadEndOfMessageSpriteSheet(content) content = await linkEndOfMessageSpriteSheet(content)
} else { } else {
// Looks like we're using the plaintext body! // Looks like we're using the plaintext body!
content = event.content.body content = event.content.body
+112 -122
View File
@@ -1,22 +1,11 @@
const assert = require("assert").strict const assert = require("assert").strict
const fs = require("fs")
const {test} = require("supertape") const {test} = require("supertape")
const DiscordTypes = require("discord-api-types/v10") const DiscordTypes = require("discord-api-types/v10")
const {eventToMessage} = require("./event-to-message") const {eventToMessage} = require("./event-to-message")
const {convertImageStream} = require("./emoji-sheet")
const data = require("../../../test/data") const data = require("../../../test/data")
const {MatrixServerError} = require("../../matrix/mreq") const {MatrixServerError} = require("../../matrix/mreq")
const {select, discord} = require("../../passthrough") const {select, discord} = require("../../passthrough")
/* c8 ignore next 7 */
function slow() {
if (process.argv.includes("--slow")) {
return test
} else {
return test.skip
}
}
/** /**
* @param {string} roomID * @param {string} roomID
* @param {string} eventID * @param {string} eventID
@@ -49,25 +38,6 @@ function sameFirstContentAndWhitespace(t, a, b) {
t.equal(a2, b2) t.equal(a2, b2)
} }
/**
* MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function mockGetAndConvertEmoji(mxc) {
const id = mxc.match(/\/([^./]*)$/)?.[1]
let s
if (fs.existsSync(`test/res/${id}.png`)) {
s = fs.createReadStream(`test/res/${id}.png`)
} else {
s = fs.createReadStream(`test/res/${id}.gif`)
}
return convertImageStream(s, () => {
s.pause()
s.emit("end")
})
}
test("event2message: body is used when there is no formatted_body", async t => { test("event2message: body is used when there is no formatted_body", async t => {
t.deepEqual( t.deepEqual(
await eventToMessage({ await eventToMessage({
@@ -5335,102 +5305,122 @@ test("event2message: table", async t => {
) )
}) })
slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet", async t => { test("event2message: unknown emoji at the end is used for sprite sheet", async t => {
const messages = await eventToMessage({ t.deepEqual(
type: "m.room.message", await eventToMessage({
sender: "@cadence:cadence.moe", type: "m.room.message",
content: { sender: "@cadence:cadence.moe",
msgtype: "m.text", content: {
body: "wrong body", msgtype: "m.text",
format: "org.matrix.custom.html", body: "wrong body",
formatted_body: 'a b <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy\" title=\":ms_robot_grin:\" alt=\":ms_robot_grin:\">' format: "org.matrix.custom.html",
}, formatted_body: 'a b <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy\" title=\":ms_robot_grin:\" alt=\":ms_robot_grin:\">'
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", },
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
}, {}, {}, {mxcDownloader: mockGetAndConvertEmoji}) room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
const testResult = { }),
content: messages.messagesToSend[0].content, {
fileName: messages.messagesToSend[0].pendingFiles[0].name, messagesToDelete: [],
fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64") messagesToEdit: [],
} messagesToSend: [{
t.deepEqual(testResult, { username: "cadence [they]",
content: "a b", content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FRLMgJGfgTPjIQtvvWZsYjhjy)",
fileName: "emojis.png", avatar_url: undefined,
fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH" allowed_mentions: {
}) parse: ["users", "roles"]
}
}],
ensureJoined: []
}
)
}) })
slow()("event2message: known emoji from an unreachable server at the end is reuploaded as a sprite sheet", async t => { test("event2message: known emoji from an unreachable server at the end is used for sprite sheet", async t => {
const messages = await eventToMessage({ t.deepEqual(
type: "m.room.message", await eventToMessage({
sender: "@cadence:cadence.moe", type: "m.room.message",
content: { sender: "@cadence:cadence.moe",
msgtype: "m.text", content: {
body: "wrong body", msgtype: "m.text",
format: "org.matrix.custom.html", body: "wrong body",
formatted_body: 'a b <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/bZFuuUSEebJYXUMSxuuSuLTa\" title=\":emoji_from_unreachable_server:\" alt=\":emoji_from_unreachable_server:\">' format: "org.matrix.custom.html",
}, formatted_body: 'a b <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/bZFuuUSEebJYXUMSxuuSuLTa\" title=\":emoji_from_unreachable_server:\" alt=\":emoji_from_unreachable_server:\">'
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", },
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
}, {}, {}, {mxcDownloader: mockGetAndConvertEmoji}) room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
const testResult = { }),
content: messages.messagesToSend[0].content, {
fileName: messages.messagesToSend[0].pendingFiles[0].name, messagesToDelete: [],
fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64") messagesToEdit: [],
} messagesToSend: [{
t.deepEqual(testResult, { username: "cadence [they]",
content: "a b", content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FbZFuuUSEebJYXUMSxuuSuLTa)",
fileName: "emojis.png", avatar_url: undefined,
fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAOoUlEQVR4nM1aCXBbx3l+Eu8bN0CAuO+TAHGTFAmAJHgT" allowed_mentions: {
}) parse: ["users", "roles"]
}
}],
ensureJoined: []
}
)
}) })
slow()("event2message: known and unknown emojis in the end are reuploaded as a sprite sheet", async t => { test("event2message: known and unknown emojis in the end are used for sprite sheet", async t => {
const messages = await eventToMessage({ t.deepEqual(
type: "m.room.message", await eventToMessage({
sender: "@cadence:cadence.moe", type: "m.room.message",
content: { sender: "@cadence:cadence.moe",
msgtype: "m.text", content: {
body: "wrong body", msgtype: "m.text",
format: "org.matrix.custom.html", body: "wrong body",
formatted_body: 'known unknown: <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC\" title=\":hippo:\" alt=\":hippo:\"> <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/wcouHVjbKJJYajkhJLsyeJAA\" title=\":ms_robot_dress:\" alt=\":ms_robot_dress:\"> and known unknown: <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc\" title=\":hipposcope:\" alt=\":hipposcope:\"> <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/HYcztccFIPgevDvoaWNsEtGJ\" title=\":ms_robot_cat:\" alt=\":ms_robot_cat:\">' format: "org.matrix.custom.html",
}, formatted_body: 'known unknown: <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC\" title=\":hippo:\" alt=\":hippo:\"> <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/wcouHVjbKJJYajkhJLsyeJAA\" title=\":ms_robot_dress:\" alt=\":ms_robot_dress:\"> and known unknown: <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc\" title=\":hipposcope:\" alt=\":hipposcope:\"> <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/HYcztccFIPgevDvoaWNsEtGJ\" title=\":ms_robot_cat:\" alt=\":ms_robot_cat:\">'
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", },
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
}, {}, {}, {mxcDownloader: mockGetAndConvertEmoji}) room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
const testResult = { }),
content: messages.messagesToSend[0].content, {
fileName: messages.messagesToSend[0].pendingFiles[0].name, messagesToDelete: [],
fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64") messagesToEdit: [],
} messagesToSend: [{
t.deepEqual(testResult, { username: "cadence [they]",
content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown:", content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown: [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FWbYqNlACRuicynBfdnPYtmvc&e=cadence.moe%2FHYcztccFIPgevDvoaWNsEtGJ)",
fileName: "emojis.png", avatar_url: undefined,
fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAT/klEQVR4nOVcC3CVRZbuS2KAIMpDQt5PQkIScm/uvYRX" allowed_mentions: {
}) parse: ["users", "roles"]
}
}],
ensureJoined: []
}
)
}) })
slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet", async t => { test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
const messages = await eventToMessage({ t.deepEqual(
type: "m.room.message", await eventToMessage({
sender: "@cadence:cadence.moe", type: "m.room.message",
content: { sender: "@cadence:cadence.moe",
msgtype: "m.text", content: {
body: "testing :chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black::chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black:", msgtype: "m.text",
format: "org.matrix.custom.html", body: "testing :chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black::chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black:",
formatted_body: "testing <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix\" title=\":chess_good_move:\" alt=\":chess_good_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU\" title=\":chess_incorrect:\" alt=\":chess_incorrect:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc\" title=\":chess_blund:\" alt=\":chess_blund:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX\" title=\":chess_brilliant_move:\" alt=\":chess_brilliant_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ\" title=\":chess_blundest:\" alt=\":chess_blundest:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj\" title=\":chess_draw_black:\" alt=\":chess_draw_black:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix\" title=\":chess_good_move:\" alt=\":chess_good_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU\" title=\":chess_incorrect:\" alt=\":chess_incorrect:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc\" title=\":chess_blund:\" alt=\":chess_blund:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX\" title=\":chess_brilliant_move:\" alt=\":chess_brilliant_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ\" title=\":chess_blundest:\" alt=\":chess_blundest:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj\" title=\":chess_draw_black:\" alt=\":chess_draw_black:\">" format: "org.matrix.custom.html",
}, formatted_body: "testing <img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix\" title=\":chess_good_move:\" alt=\":chess_good_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU\" title=\":chess_incorrect:\" alt=\":chess_incorrect:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc\" title=\":chess_blund:\" alt=\":chess_blund:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX\" title=\":chess_brilliant_move:\" alt=\":chess_brilliant_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ\" title=\":chess_blundest:\" alt=\":chess_blundest:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj\" title=\":chess_draw_black:\" alt=\":chess_draw_black:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix\" title=\":chess_good_move:\" alt=\":chess_good_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU\" title=\":chess_incorrect:\" alt=\":chess_incorrect:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc\" title=\":chess_blund:\" alt=\":chess_blund:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX\" title=\":chess_brilliant_move:\" alt=\":chess_brilliant_move:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ\" title=\":chess_blundest:\" alt=\":chess_blundest:\"><img data-mx-emoticon height=\"32\" src=\"mxc://cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj\" title=\":chess_draw_black:\" alt=\":chess_draw_black:\">"
event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4", },
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4",
}, {}, {}, {mxcDownloader: mockGetAndConvertEmoji}) room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
const testResult = { }),
content: messages.messagesToSend[0].content, {
fileName: messages.messagesToSend[0].pendingFiles[0].name, messagesToDelete: [],
fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64") messagesToEdit: [],
} messagesToSend: [{
t.deepEqual(testResult, { username: "cadence [they]",
content: "testing", content: "testing [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj&e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj)",
fileName: "emojis.png", avatar_url: undefined,
fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3hUVdr/KIpKL2nT0pPpLRNQkdXddV1c" allowed_mentions: {
}) parse: ["users", "roles"]
}
}],
ensureJoined: []
}
)
}) })
+19 -2
View File
@@ -232,11 +232,28 @@ function generatePermittedMediaHash(mxc) {
* @returns {string | undefined} * @returns {string | undefined}
*/ */
function getPublicUrlForMxc(mxc) { function getPublicUrlForMxc(mxc) {
const serverAndMediaID = generatePermittedMediaHash(mxc); const serverAndMediaID = makeMxcPublic(mxc)
if(!serverAndMediaID) return undefined if(!serverAndMediaID) return undefined
return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}` return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
} }
/**
* @param {string} mxc
* @returns {string | undefined} mxc URL with protocol stripped, e.g. "cadence.moe/abcdef1234"
*/
function makeMxcPublic(mxc) {
assert(hasher, "xxhash is not ready yet")
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (!mediaParts) return undefined
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
const unsignedHash = hasher.h64(serverAndMediaID)
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)
return serverAndMediaID
}
/** /**
* @param {string} roomVersionString * @param {string} roomVersionString
* @param {number} desiredVersion * @param {number} desiredVersion
@@ -364,7 +381,7 @@ async function setUserPowerCascade(spaceID, mxid, power, api) {
module.exports.bot = bot module.exports.bot = bot
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
module.exports.generatePermittedMediaHash = generatePermittedMediaHash module.exports.makeMxcPublic = makeMxcPublic
module.exports.getPublicUrlForMxc = getPublicUrlForMxc module.exports.getPublicUrlForMxc = getPublicUrlForMxc
module.exports.getEventIDHash = getEventIDHash module.exports.getEventIDHash = getEventIDHash
module.exports.MatrixStringBuilder = MatrixStringBuilder module.exports.MatrixStringBuilder = MatrixStringBuilder
+27 -24
View File
@@ -11,10 +11,18 @@ require("xxhash-wasm")().then(h => hasher = h)
const {sync, as, select} = require("../../passthrough") const {sync, as, select} = require("../../passthrough")
/** @type {import("../../m2d/actions/emoji-sheet")} */
const emojiSheet = sync.require("../../m2d/actions/emoji-sheet")
/** @type {import("../../m2d/converters/emoji-sheet")} */
const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet")
const schema = { const schema = {
params: z.object({ params: z.object({
server_name: z.string(), server_name: z.string(),
media_id: z.string() media_id: z.string()
}),
sheet: z.object({
e: z.array(z.string()).or(z.string())
}) })
} }
@@ -27,8 +35,16 @@ function getAPI(event) {
return event.context.api || sync.require("../../matrix/api") return event.context.api || sync.require("../../matrix/api")
} }
function verifyMediaHash(serverName, mediaId) { /**
const serverAndMediaID = `${serverName}/${mediaId}` * @param {H3Event} event
* @returns {typeof emojiSheet["getAndConvertEmoji"]}
*/
function getMxcDownloader(event) {
/* c8 ignore next */
return event.context.mxcDownloader || emojiSheet.getAndConvertEmoji
}
function verifyMediaHash(serverAndMediaID) {
const unsignedHash = hasher.h64(serverAndMediaID) const unsignedHash = hasher.h64(serverAndMediaID)
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
@@ -44,7 +60,7 @@ function verifyMediaHash(serverName, mediaId) {
as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => { as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
const params = await getValidatedRouterParams(event, schema.params.parse) const params = await getValidatedRouterParams(event, schema.params.parse)
verifyMediaHash(params.server_name, params.media_id) verifyMediaHash(`${params.server_name}/${params.media_id}`)
const api = getAPI(event) const api = getAPI(event)
const res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`) const res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`)
@@ -57,33 +73,20 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
return res.body return res.body
})) }))
const emojiSchema = z.object({ as.router.get(`/download/sheet`, defineEventHandler(async event => {
'e': z.array(z.string()).or(z.string()) const query = await getValidatedQuery(event, schema.sheet.parse)
})
const emojiSheet = sync.require("../../m2d/actions/emoji-sheet")
const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet")
as.router.get(`/emoji/matrix`, defineEventHandler(async event => {
const query = await getValidatedQuery(event, emojiSchema.parse)
/** remember that these have no mxc:// protocol in the string for space reasons */
let mxcs = query.e let mxcs = query.e
if(!Array.isArray(mxcs)) { if (!Array.isArray(mxcs)) {
mxcs = [mxcs] mxcs = [mxcs]
} }
for(let mxc of mxcs) { for (const serverAndMediaID of mxcs) {
const mediaParts = mxc.match(/^mxc:\/\/([^/]+)\/(\w+)$/) verifyMediaHash(serverAndMediaID)
if (!mediaParts) return undefined
verifyMediaHash(mediaParts[1], mediaParts[2])
} }
const buffer = await emojiSheetConverter.compositeMatrixEmojis(mxcs, emojiSheet.getAndConvertEmoji)
const contentType = 'image/png' const buffer = await emojiSheetConverter.compositeMatrixEmojis(mxcs.map(s => `mxc://${s}`), getMxcDownloader(event))
setResponseHeader(event, "Content-Type", "image/png")
setResponseStatus(event, 200)
setResponseHeader(event, "Content-Type", contentType)
setResponseHeader(event, "Transfer-Encoding", "chunked")
return buffer return buffer
})) }))
+51
View File
@@ -1,5 +1,7 @@
// @ts-check // @ts-check
const fs = require("fs")
const {convertImageStream} = require("../../m2d/converters/emoji-sheet")
const tryToCatch = require("try-to-catch") const tryToCatch = require("try-to-catch")
const {test} = require("supertape") const {test} = require("supertape")
const {router} = require("../../../test/web") const {router} = require("../../../test/web")
@@ -33,3 +35,52 @@ test("web download matrix: works if a known attachment", async t => {
t.equal(event.node.res.statusCode, 200) t.equal(event.node.res.statusCode, 200)
t.equal(event.node.res.getHeader("content-type"), "image/png") t.equal(event.node.res.getHeader("content-type"), "image/png")
}) })
/**
* MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function mockGetAndConvertEmoji(mxc) {
const id = mxc.match(/\/([^./]*)$/)?.[1]
let s
if (fs.existsSync(`test/res/${id}.png`)) {
s = fs.createReadStream(`test/res/${id}.png`)
} else {
s = fs.createReadStream(`test/res/${id}.gif`)
}
return convertImageStream(s, () => {
s.pause()
s.emit("end")
})
}
test("web sheet: single emoji", async t => {
const event = {}
const sheet = await router.test("get", "/download/sheet?e=cadence.moe%2FRLMgJGfgTPjIQtvvWZsYjhjy", {
event,
mxcDownloader: mockGetAndConvertEmoji
})
t.equal(event.node.res.statusCode, 200)
t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH")
})
test("web sheet: multiple sources", async t => {
const event = {}
const sheet = await router.test("get", "/download/sheet?e=cadence.moe%2FWbYqNlACRuicynBfdnPYtmvc&e=cadence.moe%2FHYcztccFIPgevDvoaWNsEtGJ", {
event,
mxcDownloader: mockGetAndConvertEmoji
})
t.equal(event.node.res.statusCode, 200)
t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAT/klEQVR4nOVcC3CVRZbuS2KAIMpDQt5PQkIScm/uvYRX")
})
test("web sheet: big sheet", async t => {
const event = {}
const sheet = await router.test("get", "/download/sheet?e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj&e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj", {
event,
mxcDownloader: mockGetAndConvertEmoji
})
t.equal(event.node.res.statusCode, 200)
t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3hUVdr/KIpKL2nT0pPpLRNQkdXddV1c")
})
+46 -48
View File
@@ -75,47 +75,45 @@ const file = sync.require("../src/matrix/file")
file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) } file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) }
;(async () => { ;(async () => {
/* c8 ignore start - maybe download some more test files in slow mode */ /* c8 ignore start - download some more test files in slow mode */
if (process.argv.includes("--slow")) { test("test files: download", async t => {
test("test files: download", async t => { /** @param {{url: string, to: string}[]} files */
/** @param {{url: string, to: string}[]} files */ async function allReporter(files) {
async function allReporter(files) { return new Promise(resolve => {
return new Promise(resolve => { let resolved = 0
let resolved = 0 const report = files.map(file => file.to.split("/").slice(-1)[0][0])
const report = files.map(file => file.to.split("/").slice(-1)[0][0]) files.map(download).forEach((p, i) => {
files.map(download).forEach((p, i) => { p.then(() => {
p.then(() => { report[i] = green(".")
report[i] = green(".") process.stderr.write("\r" + report.join(""))
process.stderr.write("\r" + report.join("")) if (++resolved === files.length) resolve(null)
if (++resolved === files.length) resolve(null)
})
}) })
}) })
} })
async function download({url, to}) { }
if (await fs.existsSync(to)) return async function download({url, to}) {
const res = await fetch(url) if (await fs.existsSync(to)) return
// @ts-ignore const res = await fetch(url)
await res.body.pipeTo(Writable.toWeb(fs.createWriteStream(to, {encoding: "binary"}))) // @ts-ignore
} await res.body.pipeTo(Writable.toWeb(fs.createWriteStream(to, {encoding: "binary"})))
await allReporter([ }
{url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"}, await allReporter([
{url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"}, {url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"},
{url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"}, {url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"},
{url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"}, {url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"},
{url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"}, {url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"},
{url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"}, {url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"},
{url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"}, {url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"},
{url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"}, {url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"},
{url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"}, {url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"},
{url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"}, {url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"},
{url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"}, {url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"},
{url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"}, {url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"},
{url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"}, {url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"},
{url: "https://ezgif.com/images/format-demo/butterfly.png", to: "test/res/butterfly.png"}, {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"},
]) {url: "https://ezgif.com/images/format-demo/butterfly.png", to: "test/res/butterfly.png"},
}, {timeout: 60000}) ])
} }, {timeout: 60000})
/* c8 ignore stop */ /* c8 ignore stop */
const p = migrate.migrate(db) const p = migrate.migrate(db)
@@ -135,15 +133,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("./addbot.test") require("./addbot.test")
require("../src/db/orm.test") require("../src/db/orm.test")
require("../src/web/server.test") require("../src/web/server.test")
require("../src/web/routes/download-discord.test")
require("../src/web/routes/download-matrix.test")
require("../src/web/routes/guild.test")
require("../src/web/routes/guild-settings.test")
require("../src/web/routes/info.test")
require("../src/web/routes/link.test")
require("../src/web/routes/log-in-with-matrix.test")
require("../src/web/routes/oauth.test")
require("../src/web/routes/password.test")
require("../src/discord/utils.test") require("../src/discord/utils.test")
require("../src/matrix/kstate.test") require("../src/matrix/kstate.test")
require("../src/matrix/api.test") require("../src/matrix/api.test")
@@ -178,4 +167,13 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("../src/discord/interactions/permissions.test") require("../src/discord/interactions/permissions.test")
require("../src/discord/interactions/privacy.test") require("../src/discord/interactions/privacy.test")
require("../src/discord/interactions/reactions.test") require("../src/discord/interactions/reactions.test")
require("../src/web/routes/download-discord.test")
require("../src/web/routes/download-matrix.test")
require("../src/web/routes/guild.test")
require("../src/web/routes/guild-settings.test")
require("../src/web/routes/info.test")
require("../src/web/routes/link.test")
require("../src/web/routes/log-in-with-matrix.test")
require("../src/web/routes/oauth.test")
require("../src/web/routes/password.test")
})() })()
+2 -1
View File
@@ -51,7 +51,7 @@ class Router {
/** /**
* @param {string} method * @param {string} method
* @param {string} inputUrl * @param {string} inputUrl
* @param {{event?: any, params?: any, body?: any, sessionData?: any, getOauth2Token?: any, getClient?: (string) => {user: {getGuilds: () => Promise<DiscordTypes.RESTGetAPICurrentUserGuildsResult>}}, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, createRoom?: Partial<import("../src/d2m/actions/create-room")>, createSpace?: Partial<import("../src/d2m/actions/create-space")>, headers?: any}} [options] * @param {{event?: any, params?: any, body?: any, sessionData?: any, getOauth2Token?: any, getClient?: (string) => {user: {getGuilds: () => Promise<DiscordTypes.RESTGetAPICurrentUserGuildsResult>}}, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, createRoom?: Partial<import("../src/d2m/actions/create-room")>, createSpace?: Partial<import("../src/d2m/actions/create-space")>, mxcDownloader?: import("../src/m2d/actions/emoji-sheet")["getAndConvertEmoji"], headers?: any}} [options]
*/ */
async test(method, inputUrl, options = {}) { async test(method, inputUrl, options = {}) {
const url = new URL(inputUrl, "http://a") const url = new URL(inputUrl, "http://a")
@@ -83,6 +83,7 @@ class Router {
}, },
context: { context: {
api: options.api, api: options.api,
mxcDownloader: options.mxcDownloader,
params: options.params, params: options.params,
snow: options.snow, snow: options.snow,
createRoom: options.createRoom, createRoom: options.createRoom,