Compare commits

...

4 commits

12 changed files with 55 additions and 47 deletions

2
.gitignore vendored
View file

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

1
addbot.js Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const {reg} = require("./src/matrix/read-registration") const {reg} = require("./src/matrix/read-registration")

View file

@ -58,6 +58,7 @@
"supertape": "^10.4.0" "supertape": "^10.4.0"
}, },
"scripts": { "scripts": {
"start": "node start.js",
"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",

View file

@ -55,7 +55,7 @@ For more information about features, [see the user guide.](https://gitdab.com/ca
Using WeatherStack as a thin layer between the bridge application and the Discord API lets us control exactly what data is cached in memory. Only necessary information is cached. For example, member data, user data, message content, and past edits are never stored in memory. This keeps the memory usage low and also prevents it ballooning in size over the bridge's runtime. Using WeatherStack as a thin layer between the bridge application and the Discord API lets us control exactly what data is cached in memory. Only necessary information is cached. For example, member data, user data, message content, and past edits are never stored in memory. This keeps the memory usage low and also prevents it ballooning in size over the bridge's runtime.
The bridge uses a small SQLite database to store relationships like which Discord messages correspond to which Matrix messages. This is so the bridge knows what to edit when some message is edited on Discord. Using `without rowid` on the database tables stores the index and the data in the same B-tree. Since Matrix and Discord's internal IDs are quite long, this vastly reduces storage space because those IDs do not have to be stored twice separately. Some event IDs are actually stored as xxhash integers to reduce storage requirements even more. On my personal instance of OOYE, every 100,000 messages require 16.1 MB of storage space in the SQLite database. The bridge uses a small SQLite database to store relationships like which Discord messages correspond to which Matrix messages. This is so the bridge knows what to edit when some message is edited on Discord. Using `without rowid` on the database tables stores the index and the data in the same B-tree. Since Matrix and Discord's internal IDs are quite long, this vastly reduces storage space because those IDs do not have to be stored twice separately. Some event IDs and URLs are actually stored as xxhash integers to reduce storage requirements even more. On my personal instance of OOYE, every 300,000 messages (representing a year of conversations) requires 47.3 MB of storage space in the SQLite database.
Only necessary data and columns are queried from the database. We only contact the homeserver API if the database doesn't contain what we need. Only necessary data and columns are queried from the database. We only contact the homeserver API if the database doesn't contain what we need.
@ -83,7 +83,7 @@ Follow these steps:
1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever. 1. Run `node scripts/seed.js` to check your setup and set the bot's initial state. It will prompt you for information. You only need to run this once ever.
1. Start the bridge: `node start.js` 1. Start the bridge: `npm run start`
1. Add the bot to a server - use any *one* of the following commands for an invite link: 1. Add the bot to a server - use any *one* of the following commands for an invite link:
* (in the REPL) `addbot` * (in the REPL) `addbot`
@ -106,18 +106,19 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in
## Repository structure ## Repository structure
. .
* Run this to start the bridge:
├── start.js
* Runtime configuration, like tokens and user info: * Runtime configuration, like tokens and user info:
├── registration.yaml ├── registration.yaml
* You are here! :) * You are here! :)
└── readme.md ├── readme.md
* The bridge's SQLite database is stored here:
├── ooye.db*
* Source code * Source code
└── src └── src
* The bridge's SQLite database is stored here: * Database schema:
├── db ├── db
│   ├── *.sql, *.db │   ├── orm.js, orm-defs.d.ts
│   * Migrations change the database schema when you update to a newer version of OOYE: │   * Migrations change the database schema when you update to a newer version of OOYE:
│   ├── migrate.js
│   └── migrations │   └── migrations
│       └── *.sql, *.js │       └── *.sql, *.js
* Discord-to-Matrix bridging: * Discord-to-Matrix bridging:

11
scripts/capture-message-update-events.js Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
// @ts-check // @ts-check
// **** // ****
@ -16,16 +17,16 @@ function fieldToPresenceValue(field) {
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const HeatSync = require("heatsync") const HeatSync = require("heatsync")
const config = require("../config") const {reg} = require("../src/matrix/read-registration")
const passthrough = require("../passthrough") const passthrough = require("../src/passthrough")
const sync = new HeatSync({watchFS: false}) const sync = new HeatSync({watchFS: false})
Object.assign(passthrough, {config, sync}) Object.assign(passthrough, {sync})
const DiscordClient = require("../d2m/discord-client") const DiscordClient = require("../src/d2m/discord-client")
const discord = new DiscordClient(config.discordToken, "no") const discord = new DiscordClient(reg.ooye.discord_token, "no")
passthrough.discord = discord passthrough.discord = discord
;(async () => { ;(async () => {

10
scripts/check-migrate.js Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
// @ts-check // @ts-check
// Trigger the database migration flow and exit after committing. // Trigger the database migration flow and exit after committing.
@ -5,11 +6,10 @@
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const config = require("../config") const passthrough = require("../src/passthrough")
const passthrough = require("../passthrough") const db = new sqlite("ooye.db")
const db = new sqlite("db/ooye.db") const migrate = require("../src/db/migrate")
const migrate = require("../db/migrate")
Object.assign(passthrough, {config, db }) Object.assign(passthrough, {db})
migrate.migrate(db) migrate.migrate(db)

22
scripts/migrate-from-old-bridge.js Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const assert = require("assert").strict const assert = require("assert").strict
@ -6,18 +7,17 @@ const Semaphore = require("@chriscdn/promise-semaphore")
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const HeatSync = require("heatsync") const HeatSync = require("heatsync")
const config = require("../config") const passthrough = require("../src/passthrough")
const passthrough = require("../passthrough")
const sync = new HeatSync({watchFS: false}) const sync = new HeatSync({watchFS: false})
const {reg} = require("../matrix/read-registration") const {reg} = require("../src/matrix/read-registration")
assert(reg.old_bridge) assert(reg.old_bridge)
const oldAT = reg.old_bridge.as_token const oldAT = reg.old_bridge.as_token
const newAT = reg.as_token const newAT = reg.as_token
const oldDB = new sqlite(reg.old_bridge.database) const oldDB = new sqlite(reg.old_bridge.database)
const db = new sqlite("db/ooye.db") const db = new sqlite("ooye.db")
db.exec(`CREATE TABLE IF NOT EXISTS half_shot_migration ( db.exec(`CREATE TABLE IF NOT EXISTS half_shot_migration (
discord_channel TEXT NOT NULL, discord_channel TEXT NOT NULL,
@ -25,19 +25,19 @@ db.exec(`CREATE TABLE IF NOT EXISTS half_shot_migration (
PRIMARY KEY("discord_channel") PRIMARY KEY("discord_channel")
) WITHOUT ROWID;`) ) WITHOUT ROWID;`)
Object.assign(passthrough, {config, sync, db}) Object.assign(passthrough, {sync, db})
const DiscordClient = require("../d2m/discord-client") const DiscordClient = require("../src/d2m/discord-client")
const discord = new DiscordClient(config.discordToken, "half") const discord = new DiscordClient(reg.ooye.discord_token, "half")
passthrough.discord = discord passthrough.discord = discord
/** @type {import("../d2m/actions/create-space")} */ /** @type {import("../src/d2m/actions/create-space")} */
const createSpace = sync.require("../d2m/actions/create-space") const createSpace = sync.require("../d2m/actions/create-space")
/** @type {import("../d2m/actions/create-room")} */ /** @type {import("../src/d2m/actions/create-room")} */
const createRoom = sync.require("../d2m/actions/create-room") const createRoom = sync.require("../d2m/actions/create-room")
/** @type {import("../matrix/mreq")} */ /** @type {import("../src/matrix/mreq")} */
const mreq = sync.require("../matrix/mreq") const mreq = sync.require("../matrix/mreq")
/** @type {import("../matrix/api")} */ /** @type {import("../src/matrix/api")} */
const api = sync.require("../matrix/api") const api = sync.require("../matrix/api")
const sema = new Semaphore() const sema = new Semaphore()

13
scripts/save-channel-names-to-db.js Normal file → Executable file
View file

@ -1,19 +1,20 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const HeatSync = require("heatsync") const HeatSync = require("heatsync")
const config = require("../config") const {reg} = require("../src/matrix/read-registration")
const passthrough = require("../passthrough") const passthrough = require("../src/passthrough")
const db = new sqlite("db/ooye.db") const db = new sqlite("ooye.db")
const sync = new HeatSync({watchFS: false}) const sync = new HeatSync({watchFS: false})
Object.assign(passthrough, {config, sync, db}) Object.assign(passthrough, {sync, db})
const DiscordClient = require("../d2m/discord-client") const DiscordClient = require("../src/d2m/discord-client")
const discord = new DiscordClient(config.discordToken, "no") const discord = new DiscordClient(reg.ooye.discord_token, "no")
passthrough.discord = discord passthrough.discord = discord
;(async () => { ;(async () => {

7
scripts/save-event-types-to-db.js Normal file → Executable file
View file

@ -1,16 +1,17 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const HeatSync = require("heatsync") const HeatSync = require("heatsync")
const passthrough = require("../passthrough") const passthrough = require("../src/passthrough")
const db = new sqlite("db/ooye.db") const db = new sqlite("ooye.db")
const sync = new HeatSync({watchFS: false}) const sync = new HeatSync({watchFS: false})
Object.assign(passthrough, {sync, db}) Object.assign(passthrough, {sync, db})
const api = require("../matrix/api") const api = require("../src/matrix/api")
/** @type {{event_id: string, room_id: string, event_type: string}[]} */ // @ts-ignore /** @type {{event_id: string, room_id: string, event_type: string}[]} */ // @ts-ignore
const rows = db.prepare("SELECT event_id, room_id, event_type FROM event_message INNER JOIN message_channel USING (message_id) INNER JOIN channel_room USING (channel_id)").all() const rows = db.prepare("SELECT event_id, room_id, event_type FROM event_message INNER JOIN message_channel USING (message_id) INNER JOIN channel_room USING (channel_id)").all()

12
scripts/seed.js Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const assert = require("assert").strict const assert = require("assert").strict
@ -20,11 +21,11 @@ const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]
// Move database file if it's still in the old location // Move database file if it's still in the old location
if (fs.existsSync("db")) { if (fs.existsSync("db")) {
if (fs.existsSync("db/ooye.db")) { if (fs.existsSync("db/ooye.db")) {
fs.renameSync("db/ooye.db", "src/db/ooye.db") fs.renameSync("db/ooye.db", "ooye.db")
} }
const files = fs.readdirSync("db") const files = fs.readdirSync("db")
if (files.length) { if (files.length) {
console.error("You must manually move or delete the files in the db folder:") console.error("The db folder is deprecated and must be removed. Your ooye.db database file has already been moved to the root of the repo. You must manually move or delete the remaining files:")
for (const file of files) { for (const file of files) {
console.error(file) console.error(file)
} }
@ -33,14 +34,13 @@ if (fs.existsSync("db")) {
fs.rmSync("db", {recursive: true}) fs.rmSync("db", {recursive: true})
} }
const config = require("../config")
const passthrough = require("../src/passthrough") const passthrough = require("../src/passthrough")
const db = new sqlite("src/db/ooye.db") const db = new sqlite("ooye.db")
const migrate = require("../src/db/migrate") const migrate = require("../src/db/migrate")
const sync = new HeatSync({watchFS: false}) const sync = new HeatSync({watchFS: false})
Object.assign(passthrough, { sync, config, db }) Object.assign(passthrough, {sync, db})
const orm = sync.require("../src/db/orm") const orm = sync.require("../src/db/orm")
passthrough.from = orm.from passthrough.from = orm.from
@ -225,7 +225,7 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) {
assert(utils.eventSenderIsFromDiscord(mxid), "appservice's mxid must be in the namespace it controls") assert(utils.eventSenderIsFromDiscord(mxid), "appservice's mxid must be in the namespace it controls")
assert(reg.ooye.server_origin.match(/^https?:\/\//), "server origin must start with http or https") assert(reg.ooye.server_origin.match(/^https?:\/\//), "server origin must start with http or https")
assert.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash") assert.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash")
const botID = Buffer.from(config.discordToken.split(".")[0], "base64").toString() const botID = Buffer.from(reg.ooye.discord_token.split(".")[0], "base64").toString()
assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format") assert(botID.match(/^[0-9]{10,}$/), "discord token must follow the correct format")
assert.match(reg.url, /^https?:/, "url must start with http:// or https://") assert.match(reg.url, /^https?:/, "url must start with http:// or https://")

3
scripts/wal.js Normal file → Executable file
View file

@ -1,6 +1,7 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const db = new sqlite("db/ooye.db", {fileMustExist: true}) const db = new sqlite("ooye.db", {fileMustExist: true})
db.pragma("journal_mode = wal") db.pragma("journal_mode = wal")
db.close() db.close()

3
start.js Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env node
// @ts-check // @ts-check
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
@ -6,7 +7,7 @@ const HeatSync = require("heatsync")
const {reg} = require("./src/matrix/read-registration") const {reg} = require("./src/matrix/read-registration")
const passthrough = require("./src/passthrough") const passthrough = require("./src/passthrough")
const db = new sqlite("src/db/ooye.db") const db = new sqlite("ooye.db")
/** @type {import("heatsync").default} */ // @ts-ignore /** @type {import("heatsync").default} */ // @ts-ignore
const sync = new HeatSync() const sync = new HeatSync()