Interactive initial setup

This commit is contained in:
Cadence Ember 2024-09-05 15:36:43 +12:00
parent e0bb19bfab
commit 37f3a59d8e
24 changed files with 249 additions and 43 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 {DiscordTypes.APIGuildChannel} channel
* @param {{id: string, topic?: string?}} 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(channelID, guildID)
await createRoom.unbridgeDeletedChannel({id: 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,10 +4,9 @@ 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 registration = require("../../matrix/read-registration")
const {reg} = 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 (registration.ooye.include_user_id_in_mxid) {
if (reg.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,8 +20,7 @@ const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
const utils = sync.require("./converters/utils")
/** @type {import("../matrix/api")}) */
const api = sync.require("../matrix/api")
/** @type {import("../matrix/read-registration")}) */
const reg = sync.require("../matrix/read-registration")
const {reg} = require("../matrix/read-registration")
let lastReportedEvent = 0

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,10 +5,7 @@ const mixin = require("@cloudrac3r/mixin-deep")
const stream = require("stream")
const getStream = require("get-stream")
const passthrough = require("../passthrough")
const { sync } = passthrough
/** @type {import("./read-registration")} */
const reg = sync.require("./read-registration.js")
const {reg} = require("./read-registration.js")
const baseUrl = `${reg.ooye.server_origin}/_matrix`

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,14 +1,85 @@
// @ts-check
const fs = require("fs")
const crypto = require("crypto")
const assert = require("assert").strict
const path = require("path")
const yaml = require("js-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)
const registrationFilePath = path.join(process.cwd(), "registration.yaml")
module.exports = reg
/** @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

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,10 +17,12 @@
"@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",
@ -978,6 +980,14 @@
"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",
@ -1495,6 +1505,37 @@
"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,10 +26,12 @@
"@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,14 +165,16 @@ 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.
* (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.
* (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.
@ -186,4 +188,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: 113
Total transitive production dependencies: 116

View file

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

View file

@ -1,11 +1,13 @@
// @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 HeatSync = require("heatsync")
const {prompt, Prompt} = require("enquirer")
const Input = require("enquirer/lib/prompts/input")
const fetch = require("node-fetch")
const {magenta, bold} = require("ansi-colors")
const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]})
@ -26,10 +28,8 @@ const DiscordClient = require("../d2m/discord-client")
const discord = new DiscordClient(config.discordToken, "no")
passthrough.discord = discord
const api = require("../matrix/api")
const file = require("../matrix/file")
const reg = require("../matrix/read-registration")
const utils = require("../m2d/converters/utils")
let registration = require("../matrix/read-registration")
let {reg, getTemplateRegistration, writeRegistration, readRegistration} = registration
function die(message) {
console.error(message)
@ -50,6 +50,73 @@ async function uploadAutoEmoji(guild, name, filename) {
}
;(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: async 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
}
})
/** @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 = Object.assign(template, urlResponse, {ooye: {...template.ooye, ...serverNameResponse, ...serverUrlResponse}})
registration.reg = reg
writeRegistration(reg)
}
// done with user prompts, reg is now guaranteed to be valid
console.log("Processing. This could take up to 30 seconds. Please be patient...")
const api = require("../matrix/api")
const file = require("../matrix/file")
const utils = require("../m2d/converters/utils")
const mxid = `@${reg.sender_localpart}:${reg.ooye.server_name}`
// ensure registration is correctly set...
@ -60,6 +127,9 @@ async function uploadAutoEmoji(guild, name, filename) {
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://")
// TODO: appservice ping until it works
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,6 +31,32 @@ 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