Compare commits

..

No commits in common. "d1b0fa48cf86cfed76d28fbb9e00cb3ed36ecf10" and "6b4123b84595407a5cf44a3d6f8cab5dec7f41b8" have entirely different histories.

9 changed files with 191 additions and 307 deletions

View file

@ -66,6 +66,7 @@
"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"
} }
} }

View file

@ -34,9 +34,5 @@ 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")
})() })()

View file

@ -348,38 +348,26 @@ 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 add a link that Discord will preview with a sprite sheet of emojis. * This function will strip them from the content and generate the correct pending file of the sprite sheet.
* @param {string} content * @param {string} content
* @returns {string} new content with emoji sheet link * @param {{id: string, filename: string}[]} attachments
* @param {({name: string, mxc: string} | {name: string, mxc: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles
* @param {(mxc: string) => Promise<Buffer | undefined>} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock.
*/ */
function linkEndOfMessageSpriteSheet(content) { async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, mxcDownloader) {
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*$/
while (content.match(r)) { while (content.match(r)) {
content = content.replace(r, "") content = content.replace(r, "")
} }
// Create a sprite sheet of known and unknown emojis from the end of the message
// 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. const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis, mxcDownloader)
content = content.trimEnd() // Attach it
content += " [\u2800](" // U+2800 Braille Pattern Blank is invisible on all known platforms but is digitally not a whitespace character const filename = "emojis.png"
const afterLink = ")" attachments.push({id: String(attachments.length), filename})
pendingFiles.push({name: filename, buffer})
// Make emojis URL params return content
const params = new URLSearchParams()
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
}
params.append("e", withoutMxc)
}
const url = `${reg.ooye.bridge_origin}/download/sheet?${params.toString()}`
return content + url + afterLink
} }
/** /**
@ -536,7 +524,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, pollEnd?: {messageID: string}}} di simple-as-nails dependency injection for the matrix API * @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
*/ */
async function eventToMessage(event, guild, channel, di) { async function eventToMessage(event, guild, channel, di) {
let displayName = event.sender let displayName = event.sender
@ -949,7 +937,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 linkEndOfMessageSpriteSheet(content) content = await uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, di?.mxcDownloader)
} 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

View file

@ -1,11 +1,22 @@
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
@ -38,6 +49,25 @@ 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({
@ -5305,122 +5335,102 @@ test("event2message: table", async t => {
) )
}) })
test("event2message: unknown emoji at the end is used for sprite sheet", async t => { slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet", async t => {
t.deepEqual( const messages = await eventToMessage({
await eventToMessage({ type: "m.room.message",
type: "m.room.message", sender: "@cadence:cadence.moe",
sender: "@cadence:cadence.moe", content: {
content: { msgtype: "m.text",
msgtype: "m.text", body: "wrong body",
body: "wrong body", format: "org.matrix.custom.html",
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:\">'
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",
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, {}, {mxcDownloader: mockGetAndConvertEmoji})
}), const testResult = {
{ content: messages.messagesToSend[0].content,
messagesToDelete: [], fileName: messages.messagesToSend[0].pendingFiles[0].name,
messagesToEdit: [], fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
messagesToSend: [{ }
username: "cadence [they]", t.deepEqual(testResult, {
content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FRLMgJGfgTPjIQtvvWZsYjhjy)", content: "a b",
avatar_url: undefined, fileName: "emojis.png",
allowed_mentions: { fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH"
parse: ["users", "roles"] })
}
}],
ensureJoined: []
}
)
}) })
test("event2message: known emoji from an unreachable server at the end is used for sprite sheet", async t => { slow()("event2message: known emoji from an unreachable server at the end is reuploaded as a sprite sheet", async t => {
t.deepEqual( const messages = await eventToMessage({
await eventToMessage({ type: "m.room.message",
type: "m.room.message", sender: "@cadence:cadence.moe",
sender: "@cadence:cadence.moe", content: {
content: { msgtype: "m.text",
msgtype: "m.text", body: "wrong body",
body: "wrong body", format: "org.matrix.custom.html",
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:\">'
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",
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, {}, {mxcDownloader: mockGetAndConvertEmoji})
}), const testResult = {
{ content: messages.messagesToSend[0].content,
messagesToDelete: [], fileName: messages.messagesToSend[0].pendingFiles[0].name,
messagesToEdit: [], fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
messagesToSend: [{ }
username: "cadence [they]", t.deepEqual(testResult, {
content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FbZFuuUSEebJYXUMSxuuSuLTa)", content: "a b",
avatar_url: undefined, fileName: "emojis.png",
allowed_mentions: { fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAOoUlEQVR4nM1aCXBbx3l+Eu8bN0CAuO+TAHGTFAmAJHgT"
parse: ["users", "roles"] })
}
}],
ensureJoined: []
}
)
}) })
test("event2message: known and unknown emojis in the end are used for sprite sheet", async t => { slow()("event2message: known and unknown emojis in the end are reuploaded as a sprite sheet", async t => {
t.deepEqual( const messages = await eventToMessage({
await eventToMessage({ type: "m.room.message",
type: "m.room.message", sender: "@cadence:cadence.moe",
sender: "@cadence:cadence.moe", content: {
content: { msgtype: "m.text",
msgtype: "m.text", body: "wrong body",
body: "wrong body", format: "org.matrix.custom.html",
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:\">'
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",
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, {}, {mxcDownloader: mockGetAndConvertEmoji})
}), const testResult = {
{ content: messages.messagesToSend[0].content,
messagesToDelete: [], fileName: messages.messagesToSend[0].pendingFiles[0].name,
messagesToEdit: [], fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
messagesToSend: [{ }
username: "cadence [they]", t.deepEqual(testResult, {
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)", content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown:",
avatar_url: undefined, fileName: "emojis.png",
allowed_mentions: { fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAT/klEQVR4nOVcC3CVRZbuS2KAIMpDQt5PQkIScm/uvYRX"
parse: ["users", "roles"] })
}
}],
ensureJoined: []
}
)
}) })
test("event2message: all unknown chess emojis are used for sprite sheet", async t => { slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet", async t => {
t.deepEqual( const messages = await eventToMessage({
await eventToMessage({ type: "m.room.message",
type: "m.room.message", sender: "@cadence:cadence.moe",
sender: "@cadence:cadence.moe", content: {
content: { msgtype: "m.text",
msgtype: "m.text", 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:",
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:", format: "org.matrix.custom.html",
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:\">"
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",
event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" }, {}, {}, {mxcDownloader: mockGetAndConvertEmoji})
}), const testResult = {
{ content: messages.messagesToSend[0].content,
messagesToDelete: [], fileName: messages.messagesToSend[0].pendingFiles[0].name,
messagesToEdit: [], fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
messagesToSend: [{ }
username: "cadence [they]", t.deepEqual(testResult, {
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)", content: "testing",
avatar_url: undefined, fileName: "emojis.png",
allowed_mentions: { fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3hUVdr/KIpKL2nT0pPpLRNQkdXddV1c"
parse: ["users", "roles"] })
}
}],
ensureJoined: []
}
)
}) })

View file

@ -205,19 +205,6 @@ async function getViaServersQuery(roomID, api) {
return qs return qs
} }
function generatePermittedMediaHash(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
}
/** /**
* Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL * Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL
* because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge. * because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge.
@ -232,16 +219,6 @@ function generatePermittedMediaHash(mxc) {
* @returns {string | undefined} * @returns {string | undefined}
*/ */
function getPublicUrlForMxc(mxc) { function getPublicUrlForMxc(mxc) {
const serverAndMediaID = makeMxcPublic(mxc)
if(!serverAndMediaID) return undefined
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") assert(hasher, "xxhash is not ready yet")
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (!mediaParts) return undefined if (!mediaParts) return undefined
@ -251,7 +228,7 @@ function makeMxcPublic(mxc) {
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash) db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)
return serverAndMediaID return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
} }
/** /**
@ -381,7 +358,6 @@ 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.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

View file

@ -1,7 +1,7 @@
// @ts-check // @ts-check
const assert = require("assert/strict") const assert = require("assert/strict")
const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, createError, H3Event, getValidatedQuery} = require("h3") const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError, H3Event} = require("h3")
const {z} = require("zod") const {z} = require("zod")
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
@ -11,18 +11,10 @@ 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())
}) })
} }
@ -35,16 +27,10 @@ function getAPI(event) {
return event.context.api || sync.require("../../matrix/api") return event.context.api || sync.require("../../matrix/api")
} }
/** as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
* @param {H3Event} event const params = await getValidatedRouterParams(event, schema.params.parse)
* @returns {typeof emojiSheet["getAndConvertEmoji"]}
*/
function getMxcDownloader(event) {
/* c8 ignore next */
return event.context.mxcDownloader || emojiSheet.getAndConvertEmoji
}
function verifyMediaHash(serverAndMediaID) { const serverAndMediaID = `${params.server_name}/${params.media_id}`
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
@ -55,12 +41,7 @@ function verifyMediaHash(serverAndMediaID) {
data: `The file you requested isn't permitted by this media proxy.` data: `The file you requested isn't permitted by this media proxy.`
}) })
} }
}
as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
const params = await getValidatedRouterParams(event, schema.params.parse)
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}`)
@ -72,21 +53,3 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
setResponseHeader(event, "Transfer-Encoding", "chunked") setResponseHeader(event, "Transfer-Encoding", "chunked")
return res.body return res.body
})) }))
as.router.get(`/download/sheet`, defineEventHandler(async event => {
const query = await getValidatedQuery(event, schema.sheet.parse)
/** remember that these have no mxc:// protocol in the string for space reasons */
let mxcs = query.e
if (!Array.isArray(mxcs)) {
mxcs = [mxcs]
}
for (const serverAndMediaID of mxcs) {
verifyMediaHash(serverAndMediaID)
}
const buffer = await emojiSheetConverter.compositeMatrixEmojis(mxcs.map(s => `mxc://${s}`), getMxcDownloader(event))
setResponseHeader(event, "Content-Type", "image/png")
return buffer
}))

View file

@ -1,7 +1,5 @@
// @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")
@ -35,52 +33,3 @@ 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")
})

View file

@ -75,45 +75,47 @@ 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 - download some more test files in slow mode */ /* c8 ignore start - maybe download some more test files in slow mode */
test("test files: download", async t => { if (process.argv.includes("--slow")) {
/** @param {{url: string, to: string}[]} files */ test("test files: download", async t => {
async function allReporter(files) { /** @param {{url: string, to: string}[]} files */
return new Promise(resolve => { async function allReporter(files) {
let resolved = 0 return new Promise(resolve => {
const report = files.map(file => file.to.split("/").slice(-1)[0][0]) let resolved = 0
files.map(download).forEach((p, i) => { const report = files.map(file => file.to.split("/").slice(-1)[0][0])
p.then(() => { files.map(download).forEach((p, i) => {
report[i] = green(".") p.then(() => {
process.stderr.write("\r" + report.join("")) report[i] = green(".")
if (++resolved === files.length) resolve(null) process.stderr.write("\r" + report.join(""))
if (++resolved === files.length) resolve(null)
})
}) })
}) })
}) }
} async function download({url, to}) {
async function download({url, to}) { if (await fs.existsSync(to)) return
if (await fs.existsSync(to)) return const res = await fetch(url)
const res = await fetch(url) // @ts-ignore
// @ts-ignore await res.body.pipeTo(Writable.toWeb(fs.createWriteStream(to, {encoding: "binary"})))
await res.body.pipeTo(Writable.toWeb(fs.createWriteStream(to, {encoding: "binary"}))) }
} await allReporter([
await allReporter([ {url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"},
{url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"}, {url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"},
{url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"}, {url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"},
{url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"}, {url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"},
{url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"}, {url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"},
{url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"}, {url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"},
{url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"}, {url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"},
{url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"}, {url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"},
{url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"}, {url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"},
{url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"}, {url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"},
{url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"}, {url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"},
{url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"}, {url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"},
{url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"}, {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"},
{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"},
{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)
@ -133,6 +135,15 @@ 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")
@ -167,13 +178,4 @@ 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")
})() })()

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")>, mxcDownloader?: import("../src/m2d/actions/emoji-sheet")["getAndConvertEmoji"], 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")>, 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,7 +83,6 @@ 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,