Compare commits

..

5 commits

107 changed files with 109 additions and 75 deletions

2
.gitignore vendored
View file

@ -2,6 +2,6 @@ node_modules
config.js config.js
registration.yaml registration.yaml
coverage coverage
db/ooye.db* src/db/ooye.db*
test/res/* test/res/*
!test/res/lottie* !test/res/lottie*

View file

@ -59,6 +59,6 @@
"addbot": "node addbot.js", "addbot": "node addbot.js",
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap 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", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
"cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -x 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 matrix/file.js -x matrix/api.js -x matrix/mreq.js -x d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow"
} }
} }

View file

@ -4,7 +4,7 @@ const mixin = require("@cloudrac3r/mixin-deep")
const {channelToKState, _convertNameAndTopic} = require("./create-room") const {channelToKState, _convertNameAndTopic} = require("./create-room")
const {kstateStripConditionals} = require("../../matrix/kstate") const {kstateStripConditionals} = require("../../matrix/kstate")
const {test} = require("supertape") const {test} = require("supertape")
const testData = require("../../test/data") const testData = require("../../../test/data")
const passthrough = require("../../passthrough") const passthrough = require("../../passthrough")
const {db} = passthrough const {db} = passthrough

View file

@ -4,7 +4,7 @@ const mixin = require("@cloudrac3r/mixin-deep")
const {guildToKState, ensureSpace} = require("./create-space") const {guildToKState, ensureSpace} = require("./create-space")
const {kstateStripConditionals, kstateUploadMxc} = require("../../matrix/kstate") const {kstateStripConditionals, kstateUploadMxc} = require("../../matrix/kstate")
const {test} = require("supertape") const {test} = require("supertape")
const testData = require("../../test/data") const testData = require("../../../test/data")
const passthrough = require("../../passthrough") const passthrough = require("../../passthrough")
const {db} = passthrough const {db} = passthrough

View file

@ -66,7 +66,7 @@ async function stickersToState(stickers) {
while (shortcodes.includes(shortcode)) shortcode = shortcode + "~" while (shortcodes.includes(shortcode)) shortcode = shortcode + "~"
shortcodes.push(shortcode) shortcodes.push(shortcode)
result.images[shortcodes] = { result.images[shortcode] = {
info: { info: {
mimetype: file.stickerFormat.get(sticker.format_type)?.mime || "image/png" mimetype: file.stickerFormat.get(sticker.format_type)?.mime || "image/png"
}, },

View file

@ -1,6 +1,6 @@
const {_memberToStateContent} = require("./register-user") const {_memberToStateContent} = require("./register-user")
const {test} = require("supertape") const {test} = require("supertape")
const testData = require("../../test/data") const testData = require("../../../test/data")
test("member2state: without member nick or avatar", async t => { test("member2state: without member nick or avatar", async t => {
t.deepEqual( t.deepEqual(

View file

@ -1,6 +1,6 @@
const {test} = require("supertape") const {test} = require("supertape")
const {editToChanges} = require("./edit-to-changes") const {editToChanges} = require("./edit-to-changes")
const data = require("../../test/data") const data = require("../../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
test("edit2changes: edit by webhook", async t => { test("edit2changes: edit by webhook", async t => {

View file

@ -2,7 +2,7 @@
const {test} = require("supertape") const {test} = require("supertape")
const {emojiToKey} = require("./emoji-to-key") const {emojiToKey} = require("./emoji-to-key")
const data = require("../../test/data") const data = require("../../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
test("emoji2key: unicode emoji works", async t => { test("emoji2key: unicode emoji works", async t => {

View file

@ -1,6 +1,6 @@
const {test} = require("supertape") const {test} = require("supertape")
const {messageToEvent} = require("./message-to-event") const {messageToEvent} = require("./message-to-event")
const data = require("../../test/data") const data = require("../../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
test("message2event embeds: nothing but a field", async t => { test("message2event embeds: nothing but a field", async t => {

View file

@ -1,6 +1,6 @@
const {test} = require("supertape") const {test} = require("supertape")
const {messageToEvent} = require("./message-to-event") const {messageToEvent} = require("./message-to-event")
const data = require("../../test/data") const data = require("../../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
/** /**

View file

@ -1,6 +1,6 @@
const {test} = require("supertape") const {test} = require("supertape")
const {messageToEvent} = require("./message-to-event") const {messageToEvent} = require("./message-to-event")
const data = require("../../test/data") const data = require("../../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
/** /**

View file

@ -1,5 +1,5 @@
const {test} = require("supertape") const {test} = require("supertape")
const data = require("../../test/data") const data = require("../../../test/data")
const {pinsToList} = require("./pins-to-list") const {pinsToList} = require("./pins-to-list")
test("pins2list: converts known IDs, ignores unknown IDs", t => { test("pins2list: converts known IDs, ignores unknown IDs", t => {

View file

@ -1,6 +1,6 @@
const {test} = require("supertape") const {test} = require("supertape")
const {threadToAnnouncement} = require("./thread-to-announcement") const {threadToAnnouncement} = require("./thread-to-announcement")
const data = require("../../test/data") const data = require("../../../test/data")
const Ty = require("../../types") const Ty = require("../../types")
/** /**

View file

@ -1,7 +1,7 @@
const {test} = require("supertape") const {test} = require("supertape")
const tryToCatch = require("try-to-catch") const tryToCatch = require("try-to-catch")
const assert = require("assert") const assert = require("assert")
const data = require("../../test/data") const data = require("../../../test/data")
const {userToSimName} = require("./user-to-mxid") const {userToSimName} = require("./user-to-mxid")
test("user2name: cannot create user for a webhook", async t => { test("user2name: cannot create user for a webhook", async t => {

View file

@ -10,7 +10,7 @@
*/ */
module.exports = async function(db) { module.exports = async function(db) {
const config = require("../../config") const config = require("../../../config")
const id = Buffer.from(config.discordToken.split(".")[0], "base64").toString() const id = Buffer.from(config.discordToken.split(".")[0], "base64").toString()
db.prepare("UPDATE OR REPLACE sim SET user_id = ? WHERE user_id = '0'").run(id) db.prepare("UPDATE OR REPLACE sim SET user_id = ? WHERE user_id = '0'").run(id)
} }

View file

@ -0,0 +1,8 @@
BEGIN TRANSACTION;
CREATE TABLE "media_proxy" (
"permitted_hash" INTEGER NOT NULL,
PRIMARY KEY("permitted_hash")
) WITHOUT ROWID;
COMMIT;

View file

@ -1,7 +1,7 @@
// @ts-check // @ts-check
const {test} = require("supertape") const {test} = require("supertape")
const data = require("../test/data") const data = require("../../test/data")
const {db, select, from} = require("../passthrough") const {db, select, from} = require("../passthrough")

View file

@ -4,13 +4,15 @@ const assert = require("assert").strict
const util = require("util") const util = require("util")
const DiscordTypes = require("discord-api-types/v10") const DiscordTypes = require("discord-api-types/v10")
const {reg} = require("../matrix/read-registration") const {reg} = require("../matrix/read-registration")
const {addbot} = require("../addbot") const {addbot} = require("../../addbot")
const {discord, sync, db, select} = require("../passthrough") const {discord, sync, db, select} = require("../passthrough")
/** @type {import("../matrix/api")}) */ /** @type {import("../matrix/api")}) */
const api = sync.require("../matrix/api") const api = sync.require("../matrix/api")
/** @type {import("../matrix/file")} */ /** @type {import("../matrix/file")} */
const file = sync.require("../matrix/file") const file = sync.require("../matrix/file")
/** @type {import("../m2d/converters/utils")} */
const mxUtils = sync.require("../m2d/converters/utils")
/** @type {import("../d2m/actions/create-space")} */ /** @type {import("../d2m/actions/create-space")} */
const createSpace = sync.require("../d2m/actions/create-space") const createSpace = sync.require("../d2m/actions/create-space")
/** @type {import("./utils")} */ /** @type {import("./utils")} */
@ -91,9 +93,8 @@ const commands = [{
// Current avatar // Current avatar
const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "")
const avatarURLParts = avatarEvent?.url.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
let currentAvatarMessage = let currentAvatarMessage =
( avatarURLParts ? `Current room-specific avatar: ${reg.ooye.server_origin}/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` ( avatarEvent.url ? `Current room-specific avatar: ${mxUtils.getPublicUrlForMxc(avatarEvent.url)}`
: "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.") : "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.")
// Next potential avatar // Next potential avatar

View file

@ -2,7 +2,7 @@
const DiscordTypes = require("discord-api-types/v10") const DiscordTypes = require("discord-api-types/v10")
const {discord, sync, db, select} = require("../passthrough") const {discord, sync, db, select} = require("../passthrough")
const {id} = require("../addbot") const {id} = require("../../addbot")
const matrixInfo = sync.require("./interactions/matrix-info.js") const matrixInfo = sync.require("./interactions/matrix-info.js")
const invite = sync.require("./interactions/invite.js") const invite = sync.require("./interactions/invite.js")

View file

@ -1,6 +1,6 @@
const DiscordTypes = require("discord-api-types/v10") const DiscordTypes = require("discord-api-types/v10")
const {test} = require("supertape") const {test} = require("supertape")
const data = require("../test/data") const data = require("../../test/data")
const utils = require("./utils") const utils = require("./utils")
test("is webhook message: identifies bot interaction response as not a message", t => { test("is webhook message: identifies bot interaction response as not a message", t => {

View file

@ -81,9 +81,10 @@ async function convertImageStream(streamIn, stopStream) {
giframe.feed(chunk) giframe.feed(chunk)
}) })
const frame = await giframe.getFrame() const frame = await giframe.getFrame()
const pixels = Uint8Array.from(frame.pixels)
stopStream() stopStream()
const buffer = await sharp(frame.pixels, {raw: {width: frame.width, height: frame.height, channels: 4}}) const buffer = await sharp(pixels, {raw: {width: frame.width, height: frame.height, channels: 4}})
.resize(SIZE, SIZE, {fit: "contain", background: {r: 0, g: 0, b: 0, alpha: 0}}) .resize(SIZE, SIZE, {fit: "contain", background: {r: 0, g: 0, b: 0, alpha: 0}})
.png({compressionLevel: 0}) .png({compressionLevel: 0})
.toBuffer({resolveWithObject: true}) .toBuffer({resolveWithObject: true})

View file

@ -15,6 +15,8 @@ const {sync, db, discord, select, from} = passthrough
const mxUtils = sync.require("../converters/utils") const mxUtils = sync.require("../converters/utils")
/** @type {import("../../discord/utils")} */ /** @type {import("../../discord/utils")} */
const dUtils = sync.require("../../discord/utils") const dUtils = sync.require("../../discord/utils")
/** @type {import("../../matrix/file")} */
const file = sync.require("../../matrix/file")
/** @type {import("./emoji-sheet")} */ /** @type {import("./emoji-sheet")} */
const emojiSheet = sync.require("./emoji-sheet") const emojiSheet = sync.require("./emoji-sheet")

View file

@ -3,7 +3,7 @@ const fs = require("fs")
const {test} = require("supertape") const {test} = require("supertape")
const {eventToMessage} = require("./event-to-message") const {eventToMessage} = require("./event-to-message")
const {convertImageStream} = require("./emoji-sheet") 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")

View file

@ -1,8 +1,13 @@
// @ts-check // @ts-check
const assert = require("assert").strict
const passthrough = require("../../passthrough")
const {db} = passthrough
const {reg} = require("../../matrix/read-registration") const {reg} = require("../../matrix/read-registration")
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
const assert = require("assert").strict
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore /** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
let hasher = null let hasher = null
// @ts-ignore // @ts-ignore
@ -35,16 +40,6 @@ function eventSenderIsFromDiscord(sender) {
return false return false
} }
/**
* @param {string} mxc
* @returns {string?}
*/
function getPublicUrlForMxc(mxc) {
const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (avatarURLParts) return `${reg.ooye.server_origin}/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}`
else return null
}
/** /**
* Event IDs are really big and have more entropy than we need. * Event IDs are really big and have more entropy than we need.
* If we want to store the event ID in the database, we can store a more compact version by hashing it with this. * If we want to store the event ID in the database, we can store a more compact version by hashing it with this.
@ -213,6 +208,32 @@ async function getViaServersQuery(roomID, api) {
return qs return qs
} }
/**
* 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.
* To avoid the bridge acting as a proxy for *any* media, there is a list of permitted media stored in the database.
* (The other approach would be signing the URLs with a MAC (or similar) and adding the signature, but I'm not a
* cryptographer, so I don't want to.) To reduce database disk space usage, instead of storing each permitted URL,
* we just store its xxhash as a signed (as in +/-, not signature) 64-bit integer, which fits in an SQLite integer field.
* @see https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/ background
* @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details
* @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size
* @param {string} mxc
* @returns {string?}
*/
function getPublicUrlForMxc(mxc) {
assert(hasher, "xxhash is not ready yet")
const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (!avatarURLParts) return null
const serverAndMediaID = `${avatarURLParts[1]}/${avatarURLParts[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 `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
}
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
module.exports.getPublicUrlForMxc = getPublicUrlForMxc module.exports.getPublicUrlForMxc = getPublicUrlForMxc

Some files were not shown because too many files have changed in this diff Show more