Compare commits

..

4 commits
main ... main

24 changed files with 83 additions and 168 deletions

36
package-lock.json generated
View file

@ -10,7 +10,7 @@
"license": "AGPL-3.0-or-later",
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
"@cloudrac3r/discord-markdown": "^2.6.7",
"@cloudrac3r/discord-markdown": "^2.6.6",
"@cloudrac3r/giframe": "^0.4.3",
"@cloudrac3r/html-template-tag": "^5.0.1",
"@cloudrac3r/in-your-element": "^1.1.1",
@ -35,7 +35,7 @@
"lru-cache": "^11.0.2",
"prettier-bytes": "^1.0.4",
"sharp": "^0.33.4",
"snowtransfer": "^0.14.2",
"snowtransfer": "^0.15.0",
"stream-mime-type": "^1.0.2",
"try-to-catch": "^3.0.1",
"uqr": "^0.1.2",
@ -225,9 +225,9 @@
}
},
"node_modules/@cloudrac3r/discord-markdown": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.7.tgz",
"integrity": "sha512-bWLmBYWaNEDcQfZHDz4jaAxLKA9161ruEnHo3ms6kfRw8uYku/Uz7U1xTmQ2dQF/q1PiuBvM9I37pLiotlQj8A==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.6.tgz",
"integrity": "sha512-4FNO7WmACPvcTrQjeLQLr9WRuP7JDUVUGFrRJvmAjiMs2UlUAsShfSRuU2SCqz3QqmX8vyJ06wy2hkjTTyRtbw==",
"license": "MIT",
"dependencies": {
"simple-markdown": "^0.7.3"
@ -1464,18 +1464,6 @@
"node": ">=22.0.0"
}
},
"node_modules/cloudstorm/node_modules/snowtransfer": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.15.0.tgz",
"integrity": "sha512-kEDGKtFiH5nSkHsDZonEUuDx99lUasJoZ7AGrgvE8HzVG59vjvqc//C+pjWj4DuJqTj4Q+Z1L/M/MYNim8F2VA==",
"license": "MIT",
"dependencies": {
"discord-api-types": "^0.38.21"
},
"engines": {
"node": ">=16.15.0"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@ -2731,12 +2719,12 @@
}
},
"node_modules/snowtransfer": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.14.2.tgz",
"integrity": "sha512-Fi8OdRmaIgeCj58oVej+tQAoY2I+Xp/6PAYV8X93jE/2E6Anc87SbTbDV6WZXCnuzTQz3gty8JOGz02qI7Qs9A==",
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.15.0.tgz",
"integrity": "sha512-kEDGKtFiH5nSkHsDZonEUuDx99lUasJoZ7AGrgvE8HzVG59vjvqc//C+pjWj4DuJqTj4Q+Z1L/M/MYNim8F2VA==",
"license": "MIT",
"dependencies": {
"discord-api-types": "^0.38.8"
"discord-api-types": "^0.38.21"
},
"engines": {
"node": ">=16.15.0"
@ -3088,9 +3076,9 @@
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",

View file

@ -19,7 +19,7 @@
},
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
"@cloudrac3r/discord-markdown": "^2.6.7",
"@cloudrac3r/discord-markdown": "^2.6.6",
"@cloudrac3r/giframe": "^0.4.3",
"@cloudrac3r/html-template-tag": "^5.0.1",
"@cloudrac3r/in-your-element": "^1.1.1",
@ -44,7 +44,7 @@
"lru-cache": "^11.0.2",
"prettier-bytes": "^1.0.4",
"sharp": "^0.33.4",
"snowtransfer": "^0.14.2",
"snowtransfer": "^0.15.0",
"stream-mime-type": "^1.0.2",
"try-to-catch": "^3.0.1",
"uqr": "^0.1.2",

View file

@ -12,6 +12,7 @@ if (!channelID) {
const assert = require("assert/strict")
const sqlite = require("better-sqlite3")
const path = require("path")
const backfill = new sqlite("scripts/backfill.db")
backfill.prepare("CREATE TABLE IF NOT EXISTS backfill (channel_id TEXT NOT NULL, message_id INTEGER NOT NULL, PRIMARY KEY (channel_id, message_id))").run()
@ -19,9 +20,10 @@ const HeatSync = require("heatsync")
const {reg} = require("../src/matrix/read-registration")
const passthrough = require("../src/passthrough")
const {getDatabase} = require("../src/db/database")
const sync = new HeatSync({watchFS: false})
const db = new sqlite("ooye.db")
const db = getDatabase()
Object.assign(passthrough, {sync, db})
const DiscordClient = require("../src/d2m/discord-client")

View file

@ -4,10 +4,9 @@
// Trigger the database migration flow and exit after committing.
// You can use this to run migrations locally and check the result using sqlitebrowser.
const sqlite = require("better-sqlite3")
const passthrough = require("../src/passthrough")
const db = new sqlite("ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
const migrate = require("../src/db/migrate")
Object.assign(passthrough, {db})

View file

@ -16,7 +16,8 @@ const oldAT = reg.old_bridge.as_token
const newAT = reg.as_token
const oldDB = new sqlite(reg.old_bridge.database)
const db = new sqlite("ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
db.exec(`CREATE TABLE IF NOT EXISTS half_shot_migration (
discord_channel TEXT NOT NULL,

View file

@ -3,8 +3,8 @@
const HeatSync = require("heatsync")
const sync = new HeatSync({watchFS: false})
const sqlite = require("better-sqlite3")
const db = new sqlite("db/ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
const passthrough = require("../src/passthrough")
Object.assign(passthrough, {db, sync})

View file

@ -1,12 +1,12 @@
#!/usr/bin/env node
// @ts-check
const sqlite = require("better-sqlite3")
const HeatSync = require("heatsync")
const {reg} = require("../src/matrix/read-registration")
const passthrough = require("../src/passthrough")
const db = new sqlite("ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
const sync = new HeatSync({watchFS: false})

View file

@ -1,11 +1,11 @@
#!/usr/bin/env node
// @ts-check
const sqlite = require("better-sqlite3")
const HeatSync = require("heatsync")
const passthrough = require("../src/passthrough")
const db = new sqlite("ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
const sync = new HeatSync({watchFS: false})

View file

@ -34,7 +34,8 @@ if (fs.existsSync("db")) {
}
const passthrough = require("../src/passthrough")
const db = new sqlite("ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
const migrate = require("../src/db/migrate")
const sync = new HeatSync({watchFS: false})
@ -120,28 +121,16 @@ function defineEchoHandler() {
/** @type {string} */ // @ts-ignore
const serverOrigin = await serverOriginPrompt.run()
console.log("OOYE has its own web server. It needs to be accessible on the public internet.")
console.log("What port would you like OOYE to use? You can connect your reverse proxy to this port later.")
/** @type {{socket: string | number}} */
const portResponse = await prompt({
type: "input",
name: "socket",
message: "Web server port",
initial: "6693"
})
portResponse.socket = +portResponse.socket || portResponse.socket // convert to number if numeric
const app = createApp()
app.use(defineEchoHandler())
const server = createServer(toNodeListener(app))
await server.listen(portResponse.socket)
await server.listen(6693)
console.log("Now you need to enter a public URL that OOYE's web server will live on.")
console.log("Set up your reverse proxy so that this URL accesses OOYE.")
console.log("OOYE has its own web server. It needs to be accessible on the public internet.")
console.log("You need to enter a public URL where you will be able to host this web server.")
console.log("OOYE listens on localhost:6693, so you will probably have to set up a reverse proxy.")
console.log("Examples: https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/get-started.md#appendix")
if (typeof portResponse.socket === "number") {
console.log(`Now listening on http://localhost:${portResponse.socket}. Feel free to send some test requests.`)
}
console.log("Now listening on port 6693. Feel free to send some test requests.")
/** @type {{bridge_origin: string}} */
const bridgeOriginResponse = await prompt({
type: "input",
@ -267,7 +256,6 @@ function defineEchoHandler() {
reg = {
...template,
url: bridgeOriginResponse.bridge_origin,
...portResponse,
ooye: {
...template.ooye,
...bridgeOriginResponse,

View file

@ -4,13 +4,13 @@
const {createServer} = require("http")
const EventEmitter = require("events")
const {createApp, createRouter, toNodeListener} = require("h3")
const sqlite = require("better-sqlite3")
const migrate = require("../src/db/migrate")
const HeatSync = require("heatsync")
const {reg} = require("../src/matrix/read-registration")
const passthrough = require("../src/passthrough")
const db = new sqlite("ooye.db")
const {getDatabase} = require("../src/db/database")
const db = getDatabase()
const sync = new HeatSync()

View file

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

View file

@ -40,7 +40,9 @@ const PRIVACY_ENUMS = {
const DEFAULT_PRIVACY_LEVEL = 0
const READ_ONLY_ROOM_EVENTS_DEFAULT_POWER = 50
function getReadOnlyRoomEventsDefaultPower() {
return reg.ooye.read_only_room_events_default_power ?? 50
}
/** @type {Map<string, Promise<string>>} channel ID -> Promise<room ID> */
const inflightRoomCreate = new Map()
@ -148,7 +150,7 @@ async function channelToKState(channel, guild, di) {
"m.room.join_rules/": join_rules,
/** @type {Ty.Event.M_Power_Levels} */
"m.room.power_levels/": {
events_default: everyoneCanSend ? 0 : READ_ONLY_ROOM_EVENTS_DEFAULT_POWER,
events_default: everyoneCanSend ? 0 : getReadOnlyRoomEventsDefaultPower(),
events: {
"m.reaction": 0,
"m.room.redaction": 0 // only affects redactions of own events, required to be able to un-react
@ -559,7 +561,7 @@ async function createAllForGuild(guildID) {
}
module.exports.DEFAULT_PRIVACY_LEVEL = DEFAULT_PRIVACY_LEVEL
module.exports.READ_ONLY_ROOM_EVENTS_DEFAULT_POWER = READ_ONLY_ROOM_EVENTS_DEFAULT_POWER
module.exports.getReadOnlyRoomEventsDefaultPower = getReadOnlyRoomEventsDefaultPower
module.exports.PRIVACY_ENUMS = PRIVACY_ENUMS
module.exports.createRoom = createRoom
module.exports.ensureRoom = ensureRoom

View file

@ -165,7 +165,7 @@ function memberToPowerLevel(user, member, guild, channel) {
/* PL 50 = if room is read-only but the user has been specially allowed to send messages */
const everyoneCanSend = utils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
const userCanSend = utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.SendMessages)
if (!everyoneCanSend && userCanSend) return createRoom.READ_ONLY_ROOM_EVENTS_DEFAULT_POWER
if (!everyoneCanSend && userCanSend) return createRoom.getReadOnlyRoomEventsDefaultPower()
/* PL 20 = Mention Everyone for technical reasons. */
const everyoneCanMentionEveryone = utils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)
const userCanMentionEveryone = utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)

View file

@ -408,13 +408,13 @@ async function messageToEvent(message, guild, options = {}, di) {
async function transformParsedVia(parsed) {
for (const node of parsed) {
if (node.type === "discordChannel" || node.type === "discordChannelLink") {
if (node.type === "discordChannel") {
node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get()
if (node.row?.room_id) {
node.via = await getViaServersMemo(node.row.room_id)
}
}
for (const maybeChildNodesArray of [node, node.content, node.items]) {
;for (const maybeChildNodesArray of [node, node.content, node.items]) {
if (Array.isArray(maybeChildNodesArray)) {
await transformParsedVia(maybeChildNodesArray)
}
@ -611,7 +611,7 @@ async function messageToEvent(message, guild, options = {}, di) {
const event = invite.guild_scheduled_event
if (!event) continue // the event ID provided was not valid
const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric", timeZone: reg.ooye.time_zone}) // 9 June at 3:00 pm NZT
const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric"}) // 9 June at 3:00 pm NZT
const rep = new mxUtils.MatrixStringBuilder()
// Add time

View file

@ -100,44 +100,6 @@ test("message2event: simple room mention", async t => {
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
})
test("message2event: simple room link", async t => {
let called = 0
const events = await messageToEvent(data.message.simple_room_link, data.guild.general, {}, {
api: {
async getStateEvent(roomID, type, key) {
called++
t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
return {
users: {
"@_ooye_bot:cadence.moe": 100
}
}
},
async getJoinedMembers(roomID) {
called++
t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe")
return {
joined: {
"@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null},
"@user:matrix.org": {display_name: null, avatar_url: null}
}
}
}
}
})
t.deepEqual(events, [{
$type: "m.room.message",
"m.mentions": {},
msgtype: "m.text",
body: "#worm-farm",
format: "org.matrix.custom.html",
formatted_body: '<a href="https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe?via=cadence.moe&via=matrix.org">#worm-farm</a>'
}])
t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each")
})
test("message2event: nicked room mention", async t => {
let called = 0
const events = await messageToEvent(data.message.nicked_room_mention, data.guild.general, {}, {

View file

@ -32,10 +32,13 @@ async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thr
const template = creatorMxid ? "started a thread:" : "Thread started:"
const via = await mxUtils.getViaServersQuery(threadRoomID, di.api)
let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}?${via.toString()}`
let html = `${template} <a href="https://matrix.to/#/${threadRoomID}?${via.toString()}">${thread.name}</a>`
return {
msgtype,
body,
format: "org.matrix.custom.html",
formatted_body: html,
"m.mentions": {},
...context
}

View file

@ -55,6 +55,8 @@ test("thread2announcement: no known creator, no branched from event", async t =>
t.deepEqual(content, {
msgtype: "m.text",
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
format: "org.matrix.custom.html",
formatted_body: `Thread started: <a href="https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org">test thread</a>`,
"m.mentions": {}
})
})
@ -67,6 +69,8 @@ test("thread2announcement: known creator, no branched from event", async t => {
t.deepEqual(content, {
msgtype: "m.emote",
body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
format: "org.matrix.custom.html",
formatted_body: `started a thread: <a href="https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org">test thread</a>`,
"m.mentions": {}
})
})
@ -91,6 +95,8 @@ test("thread2announcement: no known creator, branched from discord event", async
t.deepEqual(content, {
msgtype: "m.text",
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
format: "org.matrix.custom.html",
formatted_body: `Thread started: <a href="https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org">test thread</a>`,
"m.mentions": {},
"m.relates_to": {
"m.in_reply_to": {
@ -120,6 +126,8 @@ test("thread2announcement: known creator, branched from discord event", async t
t.deepEqual(content, {
msgtype: "m.emote",
body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
format: "org.matrix.custom.html",
formatted_body: `started a thread: <a href="https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org">test thread</a>`,
"m.mentions": {},
"m.relates_to": {
"m.in_reply_to": {
@ -149,6 +157,8 @@ test("thread2announcement: no known creator, branched from matrix event", async
t.deepEqual(content, {
msgtype: "m.text",
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
format: "org.matrix.custom.html",
formatted_body: `Thread started: <a href="https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org">test thread</a>`,
"m.mentions": {
user_ids: ["@cadence:cadence.moe"]
},

16
src/db/database.js Normal file
View file

@ -0,0 +1,16 @@
// @ts-check
const sqlite = require("better-sqlite3")
const path = require("path")
/**
* Create a new SQLite database instance
* @param {import("better-sqlite3").Options} [options] - SQLite options
* @returns {import("better-sqlite3").Database} Database instance
*/
function getDatabase(options = {}) {
const dataDir = process.env.OOYE_DATA_DIR || process.cwd()
return new sqlite(path.join(dataDir, "ooye.db"), options)
}
module.exports = {getDatabase}

View file

@ -5,8 +5,9 @@ const {join} = require("path")
const passthrough = require("../../passthrough")
const {id} = require("../../../addbot")
async function setupEmojis() {
const {id} = require("../../../addbot")
const {discord, db} = passthrough
const emojis = await discord.snow.assets.getAppEmojis(id)
for (const name of ["L1", "L2"]) {

View file

@ -5,7 +5,8 @@ const crypto = require("crypto")
const assert = require("assert").strict
const path = require("path")
const registrationFilePath = path.join(process.cwd(), "registration.yaml")
const dataDir = process.env.OOYE_DATA_DIR || process.cwd()
const registrationFilePath = path.join(dataDir, "registration.yaml")
/** @param {import("../types").AppServiceRegistrationConfig} reg */
function checkRegistration(reg) {

2
src/types.d.ts vendored
View file

@ -31,7 +31,7 @@ export type AppServiceRegistrationConfig = {
discord_origin?: string
discord_cdn_origin?: string,
web_password: string
time_zone?: string
read_only_room_events_default_power?: number
}
old_bridge?: {
as_token: string

View file

@ -2,13 +2,13 @@
// @ts-check
const fs = require("fs")
const sqlite = require("better-sqlite3")
const migrate = require("./src/db/migrate")
const HeatSync = require("heatsync")
const {reg} = require("./src/matrix/read-registration")
const passthrough = require("./src/passthrough")
const db = new sqlite("ooye.db")
const {getDatabase} = require("./src/db/database")
const db = getDatabase()
const sync = new HeatSync({watchFunction: fs.watchFile})

View file

@ -1398,63 +1398,6 @@ module.exports = {
attachments: [],
guild_id: "112760669178241024"
},
simple_room_link: {
type: 0,
tts: false,
timestamp: "2023-07-10T20:04:25.939000+00:00",
referenced_message: null,
pinned: false,
nonce: "1128054139385806848",
mentions: [],
mention_roles: [],
mention_everyone: false,
member: {
roles: [
"112767366235959296", "118924814567211009",
"204427286542417920", "199995902742626304",
"222168467627835392", "238028326281805825",
"259806643414499328", "265239342648131584",
"271173313575780353", "287733611912757249",
"225744901915148298", "305775031223320577",
"318243902521868288", "348651574924541953",
"349185088157777920", "378402925128712193",
"392141548932038658", "393912152173576203",
"482860581670486028", "495384759074160642",
"638988388740890635", "373336013109461013",
"530220455085473813", "454567553738473472",
"790724320824655873", "1123518980456452097",
"1040735082610167858", "695946570482450442",
"1123460940935991296", "849737964090556488"
],
premium_since: null,
pending: false,
nick: null,
mute: false,
joined_at: "2015-11-11T09:55:40.321000+00:00",
flags: 0,
deaf: false,
communication_disabled_until: null,
avatar: null
},
id: "1128054143064494233",
flags: 0,
embeds: [],
edited_timestamp: null,
content: "https://discord.com/channels/112760669178241024/1100319550446252084",
components: [],
channel_id: "266767590641238027",
author: {
username: "kumaccino",
public_flags: 128,
id: "113340068197859328",
global_name: "kumaccino",
discriminator: "0",
avatar_decoration: null,
avatar: "b48302623a12bc7c59a71328f72ccb39"
},
attachments: [],
guild_id: "112760669178241024"
},
nicked_room_mention: {
type: 0,
tts: false,

View file

@ -25,7 +25,6 @@ reg.namespaces = {
aliases: [{regex: "#_ooye_.*:cadence.moe", exclusive: true}]
}
reg.ooye.bridge_origin = "https://bridge.example.org"
reg.ooye.time_zone = "Pacific/Auckland"
const sync = new HeatSync({watchFS: false})