Compare commits
6 commits
c7fb6fd52e
...
0e701b2d54
Author | SHA1 | Date | |
---|---|---|---|
0e701b2d54 | |||
0f25e73d67 | |||
8591ea5c1f | |||
67dc31f747 | |||
706b37669b | |||
845f93e5d0 |
11 changed files with 76 additions and 21 deletions
|
@ -11,7 +11,7 @@ const speedbump = sync.require("./speedbump")
|
||||||
* @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data
|
* @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data
|
||||||
*/
|
*/
|
||||||
async function deleteMessage(data) {
|
async function deleteMessage(data) {
|
||||||
const row = select("channel_room", ["room_id", "speedbump_checked"], {channel_id: data.channel_id}).get()
|
const row = select("channel_room", ["room_id", "speedbump_checked", "thread_parent"], {channel_id: data.channel_id}).get()
|
||||||
if (!row) return
|
if (!row) return
|
||||||
|
|
||||||
const eventsToRedact = select("event_message", "event_id", {message_id: data.id}).pluck().all()
|
const eventsToRedact = select("event_message", "event_id", {message_id: data.id}).pluck().all()
|
||||||
|
@ -22,7 +22,7 @@ async function deleteMessage(data) {
|
||||||
await api.redactEvent(row.room_id, eventID)
|
await api.redactEvent(row.room_id, eventID)
|
||||||
}
|
}
|
||||||
|
|
||||||
speedbump.updateCache(data.channel_id, row.speedbump_checked)
|
await speedbump.updateCache(row.thread_parent || data.channel_id, row.speedbump_checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, db} = passthrough
|
const {discord, select, db} = passthrough
|
||||||
|
|
||||||
const SPEEDBUMP_SPEED = 4000 // 4 seconds delay
|
const SPEEDBUMP_SPEED = 4000 // 4 seconds delay
|
||||||
const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours
|
const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours
|
||||||
|
@ -33,6 +33,7 @@ const bumping = new Set()
|
||||||
/**
|
/**
|
||||||
* Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
* Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
||||||
* @param {string} messageID
|
* @param {string} messageID
|
||||||
|
* @returns whether it was deleted
|
||||||
*/
|
*/
|
||||||
async function doSpeedbump(messageID) {
|
async function doSpeedbump(messageID) {
|
||||||
bumping.add(messageID)
|
bumping.add(messageID)
|
||||||
|
@ -40,6 +41,21 @@ async function doSpeedbump(messageID) {
|
||||||
return !bumping.delete(messageID)
|
return !bumping.delete(messageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
||||||
|
* @param {string} channelID
|
||||||
|
* @param {string} messageID
|
||||||
|
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
|
||||||
|
*/
|
||||||
|
async function maybeDoSpeedbump(channelID, messageID) {
|
||||||
|
let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
|
||||||
|
if (row?.thread_parent) row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
|
||||||
|
if (!row) return {affected: false, row: null}// not affected, no speedbump
|
||||||
|
// Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from.
|
||||||
|
const affected = await doSpeedbump(messageID)
|
||||||
|
return {affected, row} // maybe affected, and there is a speedbump
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} messageID
|
* @param {string} messageID
|
||||||
*/
|
*/
|
||||||
|
@ -49,4 +65,5 @@ function onMessageDelete(messageID) {
|
||||||
|
|
||||||
module.exports.updateCache = updateCache
|
module.exports.updateCache = updateCache
|
||||||
module.exports.doSpeedbump = doSpeedbump
|
module.exports.doSpeedbump = doSpeedbump
|
||||||
|
module.exports.maybeDoSpeedbump = maybeDoSpeedbump
|
||||||
module.exports.onMessageDelete = onMessageDelete
|
module.exports.onMessageDelete = onMessageDelete
|
||||||
|
|
|
@ -246,11 +246,8 @@ module.exports = {
|
||||||
if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = select("channel_room", ["speedbump_id", "speedbump_webhook_id"], {channel_id: message.channel_id}).get()
|
const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id)
|
||||||
if (row && row.speedbump_id) {
|
if (affected) return
|
||||||
const affected = await speedbump.doSpeedbump(message.id)
|
|
||||||
if (affected) return
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await sendMessage.sendMessage(message, guild, row),
|
await sendMessage.sendMessage(message, guild, row),
|
||||||
|
@ -267,12 +264,8 @@ module.exports = {
|
||||||
if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = select("channel_room", ["speedbump_id", "speedbump_webhook_id"], {channel_id: data.channel_id}).get()
|
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
|
||||||
if (row) {
|
if (affected) return
|
||||||
// Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from.
|
|
||||||
const affected = await speedbump.doSpeedbump(data.id)
|
|
||||||
if (affected) return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes.
|
// Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes.
|
||||||
// If the message content is a string then it includes all interesting fields and is meaningful.
|
// If the message content is a string then it includes all interesting fields and is meaningful.
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
const fetch = require("node-fetch").default
|
const fetch = require("node-fetch").default
|
||||||
const mixin = require("mixin-deep")
|
const mixin = require("mixin-deep")
|
||||||
|
const stream = require("stream")
|
||||||
|
const getStream = require("get-stream")
|
||||||
|
|
||||||
const passthrough = require("../passthrough")
|
const passthrough = require("../passthrough")
|
||||||
const { sync } = passthrough
|
const { sync } = passthrough
|
||||||
|
@ -27,9 +29,15 @@ class MatrixServerError extends Error {
|
||||||
* @param {any} [extra]
|
* @param {any} [extra]
|
||||||
*/
|
*/
|
||||||
async function mreq(method, url, body, extra = {}) {
|
async function mreq(method, url, body, extra = {}) {
|
||||||
|
if (body == undefined || Object.is(body.constructor, Object)) {
|
||||||
|
body = JSON.stringify(body)
|
||||||
|
} else if (body instanceof stream.Readable && reg.ooye.content_length_workaround) {
|
||||||
|
body = await getStream.buffer(body)
|
||||||
|
}
|
||||||
|
|
||||||
const opts = mixin({
|
const opts = mixin({
|
||||||
method,
|
method,
|
||||||
body: (body == undefined || Object.is(body.constructor, Object)) ? JSON.stringify(body) : body,
|
body,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${reg.as_token}`
|
Authorization: `Bearer ${reg.as_token}`
|
||||||
}
|
}
|
||||||
|
@ -39,7 +47,18 @@ async function mreq(method, url, body, extra = {}) {
|
||||||
const res = await fetch(baseUrl + url, opts)
|
const res = await fetch(baseUrl + url, opts)
|
||||||
const root = await res.json()
|
const root = await res.json()
|
||||||
|
|
||||||
if (!res.ok || root.errcode) throw new MatrixServerError(root, {baseUrl, url, ...opts})
|
if (!res.ok || root.errcode) {
|
||||||
|
if (root.error?.includes("Content-Length")) {
|
||||||
|
console.error(`OOYE cannot stream uploads to Synapse. Please choose one of these workarounds:`
|
||||||
|
+ `\n * Run an nginx reverse proxy to Synapse, and point registration.yaml's`
|
||||||
|
+ `\n \`server_origin\` to nginx`
|
||||||
|
+ `\n * Set \`content_length_workaround: true\` in registration.yaml (this will`
|
||||||
|
+ `\n halve the speed of bridging d->m files)`)
|
||||||
|
throw new Error("Synapse is not accepting stream uploads, see the message above.")
|
||||||
|
}
|
||||||
|
delete opts.headers.Authorization
|
||||||
|
throw new MatrixServerError(root, {baseUrl, url, ...opts})
|
||||||
|
}
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -16,6 +16,7 @@
|
||||||
"deep-equal": "^2.2.3",
|
"deep-equal": "^2.2.3",
|
||||||
"discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8",
|
"discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
|
"get-stream": "^6.0.1",
|
||||||
"giframe": "github:cloudrac3r/giframe#v0.4.1",
|
"giframe": "github:cloudrac3r/giframe#v0.4.1",
|
||||||
"heatsync": "^2.4.1",
|
"heatsync": "^2.4.1",
|
||||||
"html-template-tag": "github:cloudrac3r/html-template-tag#v5.0",
|
"html-template-tag": "github:cloudrac3r/html-template-tag#v5.0",
|
||||||
|
@ -1456,6 +1457,17 @@
|
||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/giframe": {
|
"node_modules/giframe": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "git+ssh://git@github.com/cloudrac3r/giframe.git#1630f4d3b2bf5acd197409c85edd11e0da72d0a1",
|
"resolved": "git+ssh://git@github.com/cloudrac3r/giframe.git#1630f4d3b2bf5acd197409c85edd11e0da72d0a1",
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"deep-equal": "^2.2.3",
|
"deep-equal": "^2.2.3",
|
||||||
"discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8",
|
"discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
|
"get-stream": "^6.0.1",
|
||||||
"giframe": "github:cloudrac3r/giframe#v0.4.1",
|
"giframe": "github:cloudrac3r/giframe#v0.4.1",
|
||||||
"heatsync": "^2.4.1",
|
"heatsync": "^2.4.1",
|
||||||
"html-template-tag": "github:cloudrac3r/html-template-tag#v5.0",
|
"html-template-tag": "github:cloudrac3r/html-template-tag#v5.0",
|
||||||
|
|
12
readme.md
12
readme.md
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
<img src="docs/img/icon.png" height="128" width="128">
|
<img src="docs/img/icon.png" height="128" width="128">
|
||||||
|
|
||||||
Modern Matrix-to-Discord appservice bridge.
|
Modern Matrix-to-Discord appservice bridge, created by [@cadence:cadence.moe](https://matrix.to/#/@cadence:cadence.moe)
|
||||||
|
|
||||||
Created by [@cadence:cadence.moe](https://matrix.to/#/@cadence:cadence.moe) // Discuss in [#out-of-your-element:cadence.moe](https://matrix.to/#/#out-of-your-element:cadence.moe)
|
[![Releases](https://img.shields.io/gitea/v/release/cadence/out-of-your-element?gitea_url=https%3A%2F%2Fgitdab.com&style=plastic&color=green)](https://gitdab.com/cadence/out-of-your-element/releases) [![Discuss on Matrix](https://img.shields.io/badge/discuss-%23out--of--your--element-white?style=plastic)](https://matrix.to/#/#out-of-your-element:cadence.moe)
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
|
@ -76,7 +76,8 @@ Follow these steps:
|
||||||
|
|
||||||
1. [Get Node.js version 18 or later](https://nodejs.org/en/download/releases) (the version is required by the better-sqlite3 and matrix-appservice dependencies)
|
1. [Get Node.js version 18 or later](https://nodejs.org/en/download/releases) (the version is required by the better-sqlite3 and matrix-appservice dependencies)
|
||||||
|
|
||||||
1. Clone this repo and checkout a specific tag. (Development happens on main. Stabler versions are tagged.)
|
1. Clone this repo and checkout a specific tag. (Development happens on main. Stable versions are tagged.)
|
||||||
|
* The latest release tag is ![](https://img.shields.io/gitea/v/release/cadence/out-of-your-element?gitea_url=https%3A%2F%2Fgitdab.com&style=flat-square&label=%20&color=black).
|
||||||
|
|
||||||
1. Install dependencies: `npm install --save-dev` (omit --save-dev if you will not run the automated tests)
|
1. Install dependencies: `npm install --save-dev` (omit --save-dev if you will not run the automated tests)
|
||||||
|
|
||||||
|
@ -96,6 +97,10 @@ Follow these steps:
|
||||||
* $ `npm run addbot`
|
* $ `npm run addbot`
|
||||||
* $ `./addbot.sh`
|
* $ `./addbot.sh`
|
||||||
|
|
||||||
|
Now any message on Discord will create the corresponding rooms on Matrix-side. After the rooms have been created, Matrix and Discord users can chat back and forth.
|
||||||
|
|
||||||
|
To get into the rooms on your Matrix account, either add yourself to `invite` in `registration.yaml`, or use the `//invite [your mxid here]` command on Discord.
|
||||||
|
|
||||||
# Development setup
|
# Development setup
|
||||||
|
|
||||||
* Be sure to install dependencies with `--save-dev` so you can run the tests.
|
* Be sure to install dependencies with `--save-dev` so you can run the tests.
|
||||||
|
@ -164,6 +169,7 @@ Follow these steps:
|
||||||
* (8) snowtransfer: Discord API library with bring-your-own-caching that I trust.
|
* (8) snowtransfer: Discord API library with bring-your-own-caching that I trust.
|
||||||
* (0) deep-equal: It's already pulled in by supertape.
|
* (0) deep-equal: It's already pulled in by supertape.
|
||||||
* (1) discord-markdown: This is my fork!
|
* (1) discord-markdown: This is my fork!
|
||||||
|
* (0) get-stream: Only needed if content_length_workaround is true.
|
||||||
* (0) giframe: This is my fork!
|
* (0) giframe: This is my fork!
|
||||||
* (1) heatsync: Module hot-reloader that I trust.
|
* (1) heatsync: Module hot-reloader that I trust.
|
||||||
* (0) entities: Looks fine. No dependencies.
|
* (0) entities: Looks fine. No dependencies.
|
||||||
|
|
|
@ -18,6 +18,7 @@ ooye:
|
||||||
max_file_size: 5000000
|
max_file_size: 5000000
|
||||||
server_name: [the part after the colon in your matrix id, like cadence.moe]
|
server_name: [the part after the colon in your matrix id, like cadence.moe]
|
||||||
server_origin: [the full protocol and domain of your actual matrix server's location, with no trailing slash, like https://matrix.cadence.moe]
|
server_origin: [the full protocol and domain of your actual matrix server's location, with no trailing slash, like https://matrix.cadence.moe]
|
||||||
|
content_length_workaround: false
|
||||||
invite:
|
invite:
|
||||||
# uncomment this to auto-invite the named user to newly created spaces and mark them as admin (PL 100) everywhere
|
# uncomment this to auto-invite the named user to newly created spaces and mark them as admin (PL 100) everywhere
|
||||||
# - @cadence:cadence.moe
|
# - '@cadence:cadence.moe'
|
||||||
|
|
|
@ -71,7 +71,11 @@ async function uploadAutoEmoji(guild, name, filename) {
|
||||||
try {
|
try {
|
||||||
await api.register(reg.sender_localpart)
|
await api.register(reg.sender_localpart)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.errcode === "M_USER_IN_USE" || e.data?.error !== "Internal server error") throw e // "Internal server error" is the only OK error because Synapse says this if you try to register the same username twice.
|
if (e.errcode === "M_USER_IN_USE" || e.data?.error === "Internal server error") {
|
||||||
|
// "Internal server error" is the only OK error because older versions of Synapse say this if you try to register the same username twice.
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload initial images...
|
// upload initial images...
|
||||||
|
|
|
@ -14,6 +14,7 @@ const db = new sqlite(":memory:")
|
||||||
|
|
||||||
const reg = require("../matrix/read-registration")
|
const reg = require("../matrix/read-registration")
|
||||||
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
||||||
|
reg.ooye.server_name = "cadence.moe"
|
||||||
reg.ooye.invite = ["@test_auto_invite:example.org"]
|
reg.ooye.invite = ["@test_auto_invite:example.org"]
|
||||||
|
|
||||||
const sync = new HeatSync({watchFS: false})
|
const sync = new HeatSync({watchFS: false})
|
||||||
|
|
1
types.d.ts
vendored
1
types.d.ts
vendored
|
@ -21,6 +21,7 @@ export type AppServiceRegistrationConfig = {
|
||||||
max_file_size: number
|
max_file_size: number
|
||||||
server_name: string
|
server_name: string
|
||||||
server_origin: string
|
server_origin: string
|
||||||
|
content_length_workaround: boolean
|
||||||
invite: string[]
|
invite: string[]
|
||||||
}
|
}
|
||||||
old_bridge?: {
|
old_bridge?: {
|
||||||
|
|
Loading…
Reference in a new issue