Compare commits

..

No commits in common. "7d42a530e746b7daccaaf1316cc5ea4eda1c993a" and "e0bb19bfaba3d8eff463a7783d0bcccced3b8374" have entirely different histories.

25 changed files with 43 additions and 306 deletions

View file

@ -3,7 +3,7 @@
const assert = require("assert").strict
const DiscordTypes = require("discord-api-types/v10")
const Ty = require("../../types")
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough
@ -372,7 +372,7 @@ async function _unbridgeRoom(channelID) {
}
/**
* @param {{id: string, topic?: string?}} channel
* @param {DiscordTypes.APIGuildChannel} channel
* @param {string} guildID
*/
async function unbridgeDeletedChannel(channel, guildID) {

View file

@ -4,7 +4,7 @@ const assert = require("assert").strict
const {isDeepStrictEqual} = require("util")
const DiscordTypes = require("discord-api-types/v10")
const Ty = require("../../types")
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough
@ -192,7 +192,7 @@ async function syncSpaceFully(guildID) {
if (discord.channels.has(channelID)) {
await createRoom.syncRoom(channelID)
} else {
await createRoom.unbridgeDeletedChannel({id: channelID}, guildID)
await createRoom.unbridgeDeletedChannel(channelID, guildID)
}
}

View file

@ -1,7 +1,7 @@
// @ts-check
const assert = require("assert")
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
const Ty = require("../../types")
const fetch = require("node-fetch").default

View file

@ -1,7 +1,7 @@
// @ts-check
const assert = require("assert").strict
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
const DiscordTypes = require("discord-api-types/v10")
const mixin = require("@cloudrac3r/mixin-deep")

View file

@ -18,7 +18,7 @@ const lottie = sync.require("../actions/lottie")
const mxUtils = sync.require("../../m2d/converters/utils")
/** @type {import("../../discord/utils")} */
const dUtils = sync.require("../../discord/utils")
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))

View file

@ -4,9 +4,10 @@ const assert = require("assert").strict
const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough
/** @type {import("../../matrix/read-registration")} */
const reg = sync.require("../../matrix/read-registration.js")
/** @type {import("../../m2d/converters/utils")} */
const mxUtils = sync.require("../../m2d/converters/utils")
const {reg} = require("../../matrix/read-registration.js")
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))

View file

@ -1,7 +1,7 @@
// @ts-check
const assert = require("assert")
const {reg} = require("../../matrix/read-registration")
const registration = require("../../matrix/read-registration")
const passthrough = require("../../passthrough")
const {select} = passthrough
@ -26,7 +26,7 @@ function downcaseUsername(user) {
// remove leading and trailing dashes and underscores...
.replace(/(?:^[_-]*|[_-]*$)/g, "")
// If requested, also make the Discord user ID part of the username
if (reg.ooye.include_user_id_in_mxid) {
if (registration.ooye.include_user_id_in_mxid) {
downcased = user.id + "_" + downcased
}
// The new length must be at least 2 characters (in other words, it should have some content)

View file

@ -46,7 +46,7 @@ test("user2name: works on special user", t => {
})
test("user2name: includes ID if requested in config", t => {
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
reg.ooye.include_user_id_in_mxid = true
t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001", id: "123456"}), "123456_harry_styles")
t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f")

View file

@ -3,7 +3,7 @@
const assert = require("assert").strict
const util = require("util")
const DiscordTypes = require("discord-api-types/v10")
const {reg} = require("../matrix/read-registration")
const reg = require("../matrix/read-registration")
const {addbot} = require("../addbot")
const {discord, sync, db, select} = require("../passthrough")

View file

@ -1,6 +1,6 @@
// @ts-check
const {reg} = require("../../matrix/read-registration")
const reg = require("../../matrix/read-registration")
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
const assert = require("assert").strict
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore

View file

@ -20,7 +20,8 @@ const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
const utils = sync.require("./converters/utils")
/** @type {import("../matrix/api")}) */
const api = sync.require("../matrix/api")
const {reg} = require("../matrix/read-registration")
/** @type {import("../matrix/read-registration")}) */
const reg = sync.require("../matrix/read-registration")
let lastReportedEvent = 0

View file

@ -11,7 +11,6 @@ const mreq = sync.require("./mreq")
const file = sync.require("./file")
/** @type {import("./txnid")} */
const makeTxnId = sync.require("./txnid")
const {reg} = require("./read-registration.js")
/**
* @param {string} p endpoint to access
@ -296,22 +295,6 @@ async function setUserPowerCascade(roomID, mxid, power) {
}
}
async function ping() {
const res = await fetch(`${mreq.baseUrl}/client/v1/appservice/${reg.id}/ping`, {
method: "POST",
headers: {
Authorization: `Bearer ${reg.as_token}`
},
body: "{}"
})
const root = await res.json()
return {
ok: res.ok,
status: res.status,
root
}
}
module.exports.path = path
module.exports.register = register
module.exports.createRoom = createRoom
@ -335,4 +318,3 @@ module.exports.profileSetDisplayname = profileSetDisplayname
module.exports.profileSetAvatarUrl = profileSetAvatarUrl
module.exports.setUserPower = setUserPower
module.exports.setUserPowerCascade = setUserPowerCascade
module.exports.ping = ping

View file

@ -1,6 +1,6 @@
// @ts-check
const {reg} = require("../matrix/read-registration")
const reg = require("../matrix/read-registration")
const {AppService} = require("@cloudrac3r/in-your-element")
const as = new AppService(reg)
as.listen()

View file

@ -14,7 +14,7 @@ const mxUtils = sync.require("../m2d/converters/utils")
const dUtils = sync.require("../discord/utils")
/** @type {import("./kstate")} */
const ks = sync.require("./kstate")
const {reg} = require("./read-registration")
const reg = require("./read-registration")
const PREFIXES = ["//", "/"]

View file

@ -5,7 +5,10 @@ const mixin = require("@cloudrac3r/mixin-deep")
const stream = require("stream")
const getStream = require("get-stream")
const {reg} = require("./read-registration.js")
const passthrough = require("../passthrough")
const { sync } = passthrough
/** @type {import("./read-registration")} */
const reg = sync.require("./read-registration.js")
const baseUrl = `${reg.ooye.server_origin}/_matrix`
@ -78,6 +81,5 @@ async function withAccessToken(token, callback) {
}
module.exports.MatrixServerError = MatrixServerError
module.exports.baseUrl = baseUrl
module.exports.mreq = mreq
module.exports.withAccessToken = withAccessToken

View file

@ -1,7 +1,7 @@
// @ts-check
const {db, from} = require("../passthrough")
const {reg} = require("./read-registration")
const reg = require("./read-registration")
const ks = require("./kstate")
const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room")

View file

@ -1,85 +1,14 @@
// @ts-check
const fs = require("fs")
const crypto = require("crypto")
const assert = require("assert").strict
const path = require("path")
const yaml = require("js-yaml")
const registrationFilePath = path.join(process.cwd(), "registration.yaml")
/** @ts-ignore @type {import("../types").AppServiceRegistrationConfig} */
const reg = yaml.load(fs.readFileSync("registration.yaml", "utf8"))
reg["ooye"].invite = (reg.ooye.invite || []).filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line
assert(reg.ooye.max_file_size)
assert(reg.ooye.namespace_prefix)
assert(reg.ooye.server_name)
/** @param {import("../types").AppServiceRegistrationConfig} reg */
function checkRegistration(reg) {
reg["ooye"].invite = (reg.ooye.invite || []).filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line
assert(reg.ooye?.max_file_size)
assert(reg.ooye?.namespace_prefix)
assert(reg.ooye?.server_name)
assert(reg.sender_localpart?.startsWith(reg.ooye.namespace_prefix), "appservice's localpart must be in the namespace it controls")
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.match(reg.url, /^https?:/, "url must start with http:// or https://")
}
/** @param {import("../types").AppServiceRegistrationConfig} reg */
function writeRegistration(reg) {
fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2))
}
/** @returns {import("../types").InitialAppServiceRegistrationConfig} reg */
function getTemplateRegistration() {
return {
id: crypto.randomBytes(16).toString("hex"),
as_token: crypto.randomBytes(16).toString("hex"),
hs_token: crypto.randomBytes(16).toString("hex"),
namespaces: {
users: [{
exclusive: true,
regex: "@_ooye_.*:cadence.moe"
}],
aliases: [{
exclusive: true,
regex: "#_ooye_.*:cadence.moe"
}]
},
protocols: [
"discord"
],
sender_localpart: "_ooye_bot",
rate_limited: false,
ooye: {
namespace_prefix: "_ooye_",
max_file_size: 5000000,
content_length_workaround: false,
include_user_id_in_mxid: false,
invite: []
}
}
}
function readRegistration() {
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
let result = null
if (fs.existsSync(registrationFilePath)) {
const content = fs.readFileSync(registrationFilePath, "utf8")
if (content.startsWith("{")) { // Use JSON parser
result = JSON.parse(content)
checkRegistration(result)
} else { // Use YAML parser
result = yaml.load(content)
checkRegistration(result)
// Convert to JSON
writeRegistration(result)
}
}
return result
}
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
let reg = readRegistration()
module.exports.registrationFilePath = registrationFilePath
module.exports.readRegistration = readRegistration
module.exports.getTemplateRegistration = getTemplateRegistration
module.exports.writeRegistration = writeRegistration
module.exports.checkRegistration = checkRegistration
module.exports.reg = reg
module.exports = reg

View file

@ -1,5 +1,5 @@
const {test} = require("supertape")
const {reg} = require("./read-registration")
const reg = require("./read-registration")
test("reg: has necessary parameters", t => {
const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"]

41
package-lock.json generated
View file

@ -17,12 +17,10 @@
"@cloudrac3r/mixin-deep": "^3.0.0",
"@cloudrac3r/pngjs": "^7.0.3",
"@cloudrac3r/turndown": "^7.1.4",
"ansi-colors": "^4.1.3",
"better-sqlite3": "^11.1.2",
"chunk-text": "^2.0.1",
"cloudstorm": "^0.10.10",
"domino": "^2.1.6",
"enquirer": "^2.4.1",
"entities": "^5.0.0",
"get-stream": "^6.0.1",
"heatsync": "^2.5.3",
@ -980,14 +978,6 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@ -1505,37 +1495,6 @@
"once": "^1.4.0"
}
},
"node_modules/enquirer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dependencies": {
"ansi-colors": "^4.1.1",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/enquirer/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/enquirer/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/entities": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-5.0.0.tgz",

View file

@ -26,12 +26,10 @@
"@cloudrac3r/mixin-deep": "^3.0.0",
"@cloudrac3r/pngjs": "^7.0.3",
"@cloudrac3r/turndown": "^7.1.4",
"ansi-colors": "^4.1.3",
"better-sqlite3": "^11.1.2",
"chunk-text": "^2.0.1",
"cloudstorm": "^0.10.10",
"domino": "^2.1.6",
"enquirer": "^2.4.1",
"entities": "^5.0.0",
"get-stream": "^6.0.1",
"heatsync": "^2.5.3",

View file

@ -165,16 +165,14 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in
* (1) @cloudrac3r/discord-markdown: This is my fork!
* (0) @cloudrac3r/giframe: This is my fork!
* (1) @cloudrac3r/html-template-tag: This is my fork!
* (16) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It has several dependencies because HTTP servers have to do more than you'd think.
* (16) @cloudrac3r/in-your-element: This is my Matrix Appservice API library.
* (0) @cloudrac3r/mixin-deep: This is my fork! (It fixes a bug in regular mixin-deep.)
* (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs.
* (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works.
* (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer.
* (42) better-sqlite3: SQLite3 is the best database, and this is the best library for it. Really! I love it.
* (1) chunk-text: It does what I want.
* (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust.
* (0) domino: DOM implementation that's already pulled in by turndown.
* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
* (0) entities: Looks fine. No dependencies.
* (0) get-stream: Only needed if content_length_workaround is true.
* (1) heatsync: Module hot-reloader that I trust.
@ -188,4 +186,4 @@ To get into the rooms on your Matrix account, either add yourself to `invite` in
* (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well.
* (0) xxhash-wasm: Used where cryptographically secure hashing is not required.
Total transitive production dependencies: 116
Total transitive production dependencies: 113

View file

@ -11,7 +11,8 @@ const passthrough = require("../passthrough")
const sync = new HeatSync({watchFS: false})
const {reg} = require("../matrix/read-registration")
/** @type {import("../matrix/read-registration")} */
const reg = sync.require("../matrix/read-registration")
assert(reg.old_bridge)
const oldAT = reg.old_bridge.as_token
const newAT = reg.as_token

View file

@ -1,15 +1,10 @@
// @ts-check
console.log("This could take up to 30 seconds. Please be patient.")
const assert = require("assert").strict
const fs = require("fs")
const sqlite = require("better-sqlite3")
const {scheduler: {wait}} = require("timers/promises")
const {isDeepStrictEqual} = require("util")
const {prompt} = require("enquirer")
const Input = require("enquirer/lib/prompts/input")
const fetch = require("node-fetch")
const {magenta, bold, cyan} = require("ansi-colors")
const HeatSync = require("heatsync")
const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]})
@ -31,8 +26,10 @@ const DiscordClient = require("../d2m/discord-client")
const discord = new DiscordClient(config.discordToken, "no")
passthrough.discord = discord
let registration = require("../matrix/read-registration")
let {reg, getTemplateRegistration, writeRegistration, readRegistration, registrationFilePath} = registration
const api = require("../matrix/api")
const file = require("../matrix/file")
const reg = require("../matrix/read-registration")
const utils = require("../m2d/converters/utils")
function die(message) {
console.error(message)
@ -52,111 +49,7 @@ async function uploadAutoEmoji(guild, name, filename) {
return emoji
}
async function validateHomeserverOrigin(serverUrlPrompt, url) {
if (!url.match(/^https?:\/\//)) return "Must be a URL"
if (url.match(/\/$/)) return "Must not end with a slash"
process.stdout.write(magenta(" checking, please wait..."))
try {
var json = await fetch(`${url}/.well-known/matrix/client`).then(res => res.json())
let baseURL = json["m.homeserver"].base_url.replace(/\/$/, "")
if (baseURL && baseURL !== url) {
serverUrlPrompt.initial = baseURL
return `Did you mean: ${bold(baseURL)}? (Enter to accept)`
}
} catch (e) {}
try {
var res = await fetch(`${url}/_matrix/client/versions`)
} catch (e) {
return e.message
}
if (res.status !== 200) return `There is no Matrix server at that URL (${url}/_matrix/client/versions returned ${res.status})`
try {
var json = await res.json()
} catch (e) {
return `There is no Matrix server at that URL (${url}/_matrix/client/versions is not JSON)`
}
return true
}
;(async () => {
// create registration file with prompts...
if (!reg) {
console.log("What is the name of your homeserver? This is the part after : in your username.")
/** @type {{server_name: string}} */
const serverNameResponse = await prompt({
type: "input",
name: "server_name",
message: "Homeserver name"
})
console.log("What is the URL of your homeserver?")
const serverUrlPrompt = new Input({
type: "input",
name: "server_origin",
message: "Homeserver URL",
initial: () => `https://${serverNameResponse.server_name}`,
validate: url => validateHomeserverOrigin(serverUrlPrompt, url)
})
/** @type {{server_origin: string}} */ // @ts-ignore
const serverUrlResponse = await serverUrlPrompt.run()
console.log("Your Matrix homeserver needs to be able to send HTTP requests to OOYE.")
console.log("What URL should OOYE be reachable on? Usually, the default works fine,")
console.log("but you need to change this if you use multiple servers or containers.")
/** @type {{url: string}} */
const urlResponse = await prompt({
type: "input",
name: "url",
message: "URL to reach OOYE",
initial: "http://localhost:6693",
validate: url => !!url.match(/^https?:\/\//)
})
const template = getTemplateRegistration()
reg = {...template, ...urlResponse, ooye: {...template.ooye, ...serverNameResponse, ...serverUrlResponse}}
registration.reg = reg
writeRegistration(reg)
}
// Done with user prompts, reg is now guaranteed to be valid
const api = require("../matrix/api")
const file = require("../matrix/file")
const utils = require("../m2d/converters/utils")
console.log(`✅ Registration file saved as ${registrationFilePath}`)
console.log(` In ${cyan("Synapse")}, you need to add it to homeserver.yaml and ${cyan("restart Synapse")}.`)
console.log(" https://element-hq.github.io/synapse/latest/application_services.html")
console.log(` In ${cyan("Conduit")}, you need to send the file contents to the #admins room.`)
console.log(" https://docs.conduit.rs/appservices.html")
console.log()
const {as} = require("../matrix/appservice")
console.log("⏳ Waiting until homeserver registration works... (Ctrl+C to cancel)")
let itWorks = false
let lastError = null
do {
const result = await api.ping()
// If it didn't work, log details and retry after some time
itWorks = result.ok
if (!itWorks) {
// Log the full error data if the error is different to last time
if (!isDeepStrictEqual(lastError, result.root)) {
if (result.root.error) {
console.log(`\nHomeserver said: [${result.status}] ${result.root.error}`)
} else {
console.log(`\nHomeserver said: [${result.status}] ${JSON.stringify(result.root)}`)
}
lastError = result.root
} else {
process.stderr.write(".")
}
await wait(5000)
}
} while (!itWorks)
console.log("")
as.close().catch(() => {})
console.log("⏩ Processing. This could take up to 30 seconds. Please be patient...")
const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}`
// ensure registration is correctly set...
@ -167,7 +60,6 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) {
const botID = Buffer.from(config.discordToken.split(".")[0], "base64").toString()
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://")
console.log("✅ Configuration looks good...")
// database ddl...

View file

@ -17,7 +17,7 @@ const config = require("../config")
const passthrough = require("../passthrough")
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_name = "cadence.moe"
reg.id = "baby" // don't actually take authenticated actions on the server
@ -117,7 +117,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("../matrix/kstate.test")
require("../matrix/api.test")
require("../matrix/file.test")
//require("../matrix/power.test")
require("../matrix/power.test")
require("../matrix/read-registration.test")
require("../matrix/txnid.test")
require("../d2m/actions/create-room.test")

26
types.d.ts vendored
View file

@ -31,32 +31,6 @@ export type AppServiceRegistrationConfig = {
}
}
export type InitialAppServiceRegistrationConfig = {
id: string
as_token: string
hs_token: string
sender_localpart: string
namespaces: {
users: {
exclusive: boolean
regex: string
}[]
aliases: {
exclusive: boolean
regex: string
}[]
}
protocols: [string]
rate_limited: boolean
ooye: {
namespace_prefix: string
max_file_size: number,
content_length_workaround: boolean,
invite: string[],
include_user_id_in_mxid: boolean
}
}
export type WebhookCreds = {
id: string
token: string