diff --git a/readme.md b/readme.md index e8a8e7e..8c9fc90 100644 --- a/readme.md +++ b/readme.md @@ -38,7 +38,6 @@ For more information about features, [see the user guide.](https://gitdab.com/ca * This bridge is not designed for puppetting. * Direct Messaging is not supported until I figure out a good way of doing it. -* Encrypted messages are not supported. Decryption is often unreliable on Matrix, and your messages end up in plaintext on Discord anyway, so there's not much advantage. ## Get started! diff --git a/scripts/estimate-size.js b/scripts/estimate-size.js deleted file mode 100644 index 341abc0..0000000 --- a/scripts/estimate-size.js +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-check - -const pb = require("prettier-bytes") -const sqlite = require("better-sqlite3") -const HeatSync = require("heatsync") - -const {reg} = require("../src/matrix/read-registration") -const passthrough = require("../src/passthrough") - -const sync = new HeatSync({watchFS: false}) -Object.assign(passthrough, {reg, sync}) - -const DiscordClient = require("../src/d2m/discord-client") - -const discord = new DiscordClient(reg.ooye.discord_token, "no") -passthrough.discord = discord - -const db = new sqlite("ooye.db") -passthrough.db = db - -const api = require("../src/matrix/api") - -const {room: roomID} = require("minimist")(process.argv.slice(2), {string: ["room"]}) -if (!roomID) { - console.error("Usage: ./scripts/estimate-size.js --room=") - process.exit(1) -} - -const {channel_id, guild_id} = db.prepare("SELECT channel_id, guild_id FROM channel_room WHERE room_id = ?").get(roomID) - -const max = 1000 - -;(async () => { - let total = 0 - let size = 0 - let from - - while (total < max) { - const events = await api.getEvents(roomID, "b", {limit: 1000, from}) - total += events.chunk.length - from = events.end - console.log(`Fetched ${total} events so far`) - - for (const e of events.chunk) { - if (e.content?.info?.size) { - size += e.content.info.size - } - } - - if (events.chunk.length === 0 || !events.end) break - } - - console.log(`Total size of uploads: ${pb(size)}`) - - const searchResults = await discord.snow.requestHandler.request(`/guilds/${guild_id}/messages/search`, { - channel_id, - offset: "0", - limit: "1" - }, "get", "json") - - const totalAllTime = searchResults.total_results - const fractionCounted = total / totalAllTime - console.log(`That counts for ${(fractionCounted*100).toFixed(2)}% of the history on Discord (${totalAllTime.toLocaleString()} messages)`) - console.log(`The size of uploads for the whole history would be approx: ${pb(Math.floor(size/total*totalAllTime))}`) -})() diff --git a/scripts/setup.js b/scripts/setup.js index 69b62a2..4e6de0a 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -1,7 +1,6 @@ #!/usr/bin/env node // @ts-check -const Ty = require("../src/types") const assert = require("assert").strict const fs = require("fs") const sqlite = require("better-sqlite3") @@ -286,8 +285,8 @@ function defineEchoHandler() { console.log() // Done with user prompts, reg is now guaranteed to be valid - const mreq = require("../src/matrix/mreq") const api = require("../src/matrix/api") + const file = require("../src/matrix/file") const DiscordClient = require("../src/d2m/discord-client") const discord = new DiscordClient(reg.ooye.discord_token, "no") passthrough.discord = discord @@ -344,13 +343,7 @@ function defineEchoHandler() { await api.register(reg.sender_localpart) // upload initial images... - const avatarBuffer = await fs.promises.readFile(join(__dirname, "..", "docs", "img", "icon.png"), null) - /** @type {Ty.R.FileUploaded} */ - const root = await mreq.mreq("POST", "/media/v3/upload", avatarBuffer, { - headers: {"Content-Type": "image/png"} - }) - const avatarUrl = root.content_uri - assert(avatarUrl) + const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element.png") console.log("✅ Matrix appservice login works...") @@ -359,7 +352,8 @@ function defineEchoHandler() { console.log("✅ Emojis are ready...") // set profile data on discord... - await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")}) + const avatarImageBuffer = await fetch("https://cadence.moe/friends/out_of_your_element.png").then(res => res.arrayBuffer()) + await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + Buffer.from(avatarImageBuffer).toString("base64")}) console.log("✅ Discord profile updated...") // set profile data on homeserver... diff --git a/src/d2m/converters/user-to-mxid.js b/src/d2m/converters/user-to-mxid.js index 7705aff..12891c0 100644 --- a/src/d2m/converters/user-to-mxid.js +++ b/src/d2m/converters/user-to-mxid.js @@ -20,10 +20,10 @@ const SPECIAL_USER_MAPPINGS = new Map([ function downcaseUsername(user) { // First, try to convert the username to the set of allowed characters let downcased = user.username.toLowerCase() - // spaces and slashes to underscores... - .replace(/[ /]/g, "_") + // spaces to underscores... + .replace(/ /g, "_") // remove disallowed characters... - .replace(/[^a-z0-9._=-]*/g, "") + .replace(/[^a-z0-9._=/-]*/g, "") // remove leading and trailing dashes and underscores... .replace(/(?:^[_-]*|[_-]*$)/g, "") // If requested, also make the Discord user ID part of the username diff --git a/src/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js index f8cf16a..387d472 100644 --- a/src/d2m/converters/user-to-mxid.test.js +++ b/src/d2m/converters/user-to-mxid.test.js @@ -21,12 +21,8 @@ test("user2name: works on single emoji at the end", t => { t.equal(userToSimName({username: "Melody 🎵", discriminator: "2192"}), "melody") }) -test("user2name: works on really weird name", t => { - t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7") -}) - -test("user2name: treats slashes", t => { - t.equal(userToSimName({username: "Evil Lillith (she/her)", discriminator: "5892"}), "evil_lillith_she_her") +test("user2name: works on crazy name", t => { + t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//") }) test("user2name: adds discriminator if name is unavailable (old tag format)", t => { diff --git a/src/db/migrations/0034-slash-not-allowed-in-mxid.sql b/src/db/migrations/0034-slash-not-allowed-in-mxid.sql deleted file mode 100644 index ea2d031..0000000 --- a/src/db/migrations/0034-slash-not-allowed-in-mxid.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN TRANSACTION; - -DELETE FROM sim WHERE sim_name like '%/%'; - -COMMIT; diff --git a/src/m2d/actions/sticker.js b/src/m2d/actions/sticker.js deleted file mode 100644 index 341d8b0..0000000 --- a/src/m2d/actions/sticker.js +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-check - -const {Readable} = require("stream") -const {ReadableStream} = require("stream/web") - -const {sync} = require("../../passthrough") -const sharp = require("sharp") -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") -/** @type {import("../../matrix/mreq")} */ -const mreq = sync.require("../../matrix/mreq") -const streamMimeType = require("stream-mime-type") - -const WIDTH = 160 -const HEIGHT = 160 -/** - * Downloads the sticker from the web and converts to webp data. - * @param {string} mxc a single mxc:// URL - * @returns {Promise} sticker webp data, or undefined if the downloaded sticker is not valid - */ -async function getAndResizeSticker(mxc) { - const res = await api.getMedia(mxc) - if (res.status !== 200) { - const root = await res.json() - throw new mreq.MatrixServerError(root, {mxc}) - } - - const streamIn = Readable.fromWeb(res.body) - const { stream, mime } = await streamMimeType.getMimeType(streamIn) - const animated = ["image/gif", "image/webp"].includes(mime) - - const transformer = sharp({animated: animated}) - .resize(WIDTH, HEIGHT, {fit: "inside", background: {r: 0, g: 0, b: 0, alpha: 0}}) - .webp() - stream.pipe(transformer) - return Readable.toWeb(transformer) -} - - -module.exports.getAndResizeSticker = getAndResizeSticker diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 81ad48c..2add279 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -631,10 +631,23 @@ async function eventToMessage(event, guild, channel, di) { } if (event.type === "m.sticker") { - const withoutMxc = mxUtils.makeMxcPublic(event.content.url) - assert(withoutMxc) - const url = `${reg.ooye.bridge_origin}/download/sticker/${withoutMxc}/_.webp` - content = `[${event.content.body || "\u2800"}](${url})` + content = "" + let filename = event.content.body + if (event.type === "m.sticker") { + let mimetype + if (event.content.info?.mimetype?.includes("/")) { + mimetype = event.content.info.mimetype + } else { + const res = await di.api.getMedia(event.content.url, {method: "HEAD"}) + if (res.status === 200) { + mimetype = res.headers.get("content-type") + } + if (!mimetype) throw new Error(`Server error ${res.status} or missing content-type while detecting sticker mimetype`) + } + filename += "." + mimetype.split("/")[1] + } + attachments.push({id: "0", filename}) + pendingFiles.push({name: filename, mxc: event.content.url}) } else if (event.type === "org.matrix.msc3381.poll.start") { const pollContent = event.content["org.matrix.msc3381.poll.start"] // just for convenience diff --git a/src/matrix/api.js b/src/matrix/api.js index 87bbf0c..70cb50b 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -136,7 +136,7 @@ async function getEventForTimestamp(roomID, ts) { */ async function getEvents(roomID, dir, pagination = {}, filter) { filter = filter && JSON.stringify(filter) - /** @type {Ty.MessagesPagination>} */ + /** @type {Ty.Pagination>} */ const root = await mreq.mreq("GET", path(`/client/v3/rooms/${roomID}/messages`, null, {...pagination, dir, filter})) return root } diff --git a/src/types.d.ts b/src/types.d.ts index a85907d..6ee2eb1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -498,13 +498,7 @@ export type Membership = "invite" | "knock" | "join" | "leave" | "ban" export type Pagination = { chunk: T[] next_batch?: string - prev_batch?: string -} - -export type MessagesPagination = { - chunk: T[] - start: string - end?: string + prev_match?: string } export type HierarchyPagination = { diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js index 82e2f7e..bb6b850 100644 --- a/src/web/routes/download-matrix.js +++ b/src/web/routes/download-matrix.js @@ -16,9 +16,6 @@ const emojiSheet = sync.require("../../m2d/actions/emoji-sheet") /** @type {import("../../m2d/converters/emoji-sheet")} */ const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet") -/** @type {import("../../m2d/actions/sticker")} */ -const sticker = sync.require("../../m2d/actions/sticker") - const schema = { params: z.object({ server_name: z.string(), @@ -26,10 +23,6 @@ const schema = { }), sheet: z.object({ e: z.array(z.string()).or(z.string()) - }), - sticker: z.object({ - server_name: z.string().regex(/^[^/]+$/), - media_id: z.string().regex(/^[A-Za-z0-9_-]+$/) }) } @@ -97,14 +90,3 @@ as.router.get(`/download/sheet`, defineEventHandler(async event => { setResponseHeader(event, "Content-Type", "image/png") return buffer })) - -as.router.get(`/download/sticker/:server_name/:media_id/_.webp`, defineEventHandler(async event => { - const {server_name, media_id} = await getValidatedRouterParams(event, schema.sticker.parse) - /** remember that this has no mxc:// protocol in the string */ - const mxc = server_name + "/" + media_id - verifyMediaHash(mxc) - - const stream = await sticker.getAndResizeSticker(`mxc://${mxc}`) - setResponseHeader(event, "Content-Type", "image/webp") - return stream -})) diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index a5508c4..dfb393b 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -115,7 +115,7 @@ function getChannelRoomsLinks(guild, rooms, roles) { let removedWrongTypeChannels = dUtils.filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type)) let removedPrivateChannels = dUtils.filterTo(unlinkedChannels, c => { const permissions = dUtils.getPermissions(guild.id, roles, guild.roles, botID, c["permission_overwrites"]) - return dUtils.hasSomePermissions(permissions, ["Administrator", "ViewChannel"]) + return dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) }) unlinkedChannels.sort((a, b) => getPosition(a, discord.channels) - getPosition(b, discord.channels))