308 lines
12 KiB
JavaScript
308 lines
12 KiB
JavaScript
// @ts-check
|
|
|
|
const assert = require("assert").strict
|
|
const fs = require("fs")
|
|
const sqlite = require("better-sqlite3")
|
|
const {scheduler} = require("timers/promises")
|
|
const {isDeepStrictEqual} = require("util")
|
|
const {createServer} = require("http")
|
|
|
|
const {prompt} = require("enquirer")
|
|
const Input = require("enquirer/lib/prompts/input")
|
|
const fetch = require("node-fetch").default
|
|
const {magenta, bold, cyan} = require("ansi-colors")
|
|
const HeatSync = require("heatsync")
|
|
const {SnowTransfer} = require("snowtransfer")
|
|
const {createApp, defineEventHandler, toNodeListener} = require("h3")
|
|
|
|
const args = require("minimist")(process.argv.slice(2), {string: ["emoji-guild"]})
|
|
|
|
// Move database file if it's still in the old location
|
|
if (fs.existsSync("db")) {
|
|
if (fs.existsSync("db/ooye.db")) {
|
|
fs.renameSync("db/ooye.db", "src/db/ooye.db")
|
|
}
|
|
const files = fs.readdirSync("db")
|
|
if (files.length) {
|
|
console.error("You must manually move or delete the files in the db folder:")
|
|
for (const file of files) {
|
|
console.error(file)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
fs.rmSync("db", {recursive: true})
|
|
}
|
|
|
|
const config = require("../config")
|
|
const passthrough = require("../src/passthrough")
|
|
const db = new sqlite("src/db/ooye.db")
|
|
const migrate = require("../src/db/migrate")
|
|
|
|
const sync = new HeatSync({watchFS: false})
|
|
|
|
Object.assign(passthrough, { sync, config, db })
|
|
|
|
const orm = sync.require("../src/db/orm")
|
|
passthrough.from = orm.from
|
|
passthrough.select = orm.select
|
|
|
|
let registration = require("../src/matrix/read-registration")
|
|
let {reg, getTemplateRegistration, writeRegistration, readRegistration, checkRegistration, registrationFilePath} = registration
|
|
|
|
function die(message) {
|
|
console.error(message)
|
|
process.exit(1)
|
|
}
|
|
|
|
async function uploadAutoEmoji(snow, guild, name, filename) {
|
|
let emoji = guild.emojis.find(e => e.name === name)
|
|
if (!emoji) {
|
|
console.log(` Uploading ${name}...`)
|
|
const data = fs.readFileSync(filename, null)
|
|
emoji = await snow.guildAssets.createEmoji(guild.id, {name, image: "data:image/png;base64," + data.toString("base64")})
|
|
} else {
|
|
console.log(` Reusing ${name}...`)
|
|
}
|
|
db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES (?, ?, ?)").run(emoji.name, emoji.id, guild.id)
|
|
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 serverOriginPrompt = new Input({
|
|
type: "input",
|
|
name: "server_origin",
|
|
message: "Homeserver URL",
|
|
initial: () => `https://${serverNameResponse.server_name}`,
|
|
validate: url => validateHomeserverOrigin(serverOriginPrompt, url)
|
|
})
|
|
/** @type {string} */ // @ts-ignore
|
|
const serverOrigin = await serverOriginPrompt.run()
|
|
|
|
const app = createApp()
|
|
app.use(defineEventHandler(() => "Out Of Your Element is listening.\n"))
|
|
const server = createServer(toNodeListener(app))
|
|
await server.listen(6693)
|
|
|
|
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("Now listening on port 6693. Feel free to send some test requests.")
|
|
/** @type {{bridge_origin: string}} */
|
|
const bridgeOriginResponse = await prompt({
|
|
type: "input",
|
|
name: "bridge_origin",
|
|
message: "URL to reach OOYE",
|
|
initial: () => `https://bridge.${serverNameResponse.server_name}`,
|
|
validate: async url => {
|
|
process.stdout.write(magenta(" checking, please wait..."))
|
|
try {
|
|
const res = await fetch(url)
|
|
if (res.status !== 200) return `Server returned status code ${res.status}`
|
|
const text = await res.text()
|
|
if (text !== "Out Of Your Element is listening.\n") return `Server does not point to OOYE`
|
|
return true
|
|
} catch (e) {
|
|
return e.message
|
|
}
|
|
}
|
|
})
|
|
|
|
await server.close()
|
|
|
|
console.log("What is your Discord bot token?")
|
|
/** @type {{discord_token: string}} */
|
|
const discordTokenResponse = await prompt({
|
|
type: "input",
|
|
name: "discord_token",
|
|
message: "Bot token",
|
|
validate: async token => {
|
|
process.stdout.write(magenta(" checking, please wait..."))
|
|
try {
|
|
const snow = new SnowTransfer(token)
|
|
await snow.user.getSelf()
|
|
return true
|
|
} catch (e) {
|
|
return e.message
|
|
}
|
|
}
|
|
})
|
|
const template = getTemplateRegistration()
|
|
reg = {...template, url: bridgeOriginResponse.bridge_origin, ooye: {...template.ooye, ...serverNameResponse, ...bridgeOriginResponse, server_origin: serverOrigin, ...discordTokenResponse}}
|
|
registration.reg = reg
|
|
checkRegistration(reg)
|
|
writeRegistration(reg)
|
|
console.log(`✅ Registration file saved as ${registrationFilePath}`)
|
|
} else {
|
|
console.log(`✅ Valid registration file found at ${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()
|
|
|
|
// Done with user prompts, reg is now guaranteed to be valid
|
|
const api = require("../src/matrix/api")
|
|
const file = require("../src/matrix/file")
|
|
const utils = require("../src/m2d/converters/utils")
|
|
const DiscordClient = require("../src/d2m/discord-client")
|
|
const discord = new DiscordClient(reg.ooye.discord_token, "no")
|
|
passthrough.discord = discord
|
|
|
|
const {as} = require("../src/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().catch(e => ({ok: false, status: "net", root: e.message}))
|
|
// 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 (typeof result.root === "string") {
|
|
console.log(`\nCannot reach homeserver: ${result.root}`)
|
|
} else 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 scheduler.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...
|
|
assert(reg.sender_localpart.startsWith(reg.ooye.namespace_prefix), "appservice's localpart 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.notEqual(reg.ooye.server_origin.slice(-1), "/", "server origin must not end in slash")
|
|
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...
|
|
await migrate.migrate(db)
|
|
|
|
// add initial rows to database, like adding the bot to sim...
|
|
db.prepare("INSERT OR IGNORE INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(botID, reg.sender_localpart.slice(reg.ooye.namespace_prefix.length), reg.sender_localpart, mxid)
|
|
|
|
console.log("✅ Database is ready...")
|
|
|
|
// ensure appservice bot user is registered...
|
|
try {
|
|
await api.register(reg.sender_localpart)
|
|
} catch (e) {
|
|
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...
|
|
const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element.png")
|
|
|
|
console.log("✅ Matrix appservice login works...")
|
|
|
|
// upload the L1 L2 emojis to some guild
|
|
const emojis = db.prepare("SELECT name FROM auto_emoji WHERE name = 'L1' OR name = 'L2'").pluck().all()
|
|
if (emojis.length !== 2) {
|
|
// If an argument was supplied, always use that one
|
|
let guild = null
|
|
if (args["emoji-guild"]) {
|
|
if (typeof args["emoji-guild"] === "string") {
|
|
guild = await discord.snow.guild.getGuild(args["emoji-guild"])
|
|
}
|
|
if (!guild) return die(`Error: You asked emojis to be uploaded to guild ID ${args["emoji-guild"]}, but the bot isn't in that guild.`)
|
|
}
|
|
// Otherwise, check if we have already registered an auto emoji guild
|
|
if (!guild) {
|
|
const guildID = passthrough.select("auto_emoji", "guild_id", {name: "_"}).pluck().get()
|
|
if (guildID) {
|
|
guild = await discord.snow.guild.getGuild(guildID, false)
|
|
}
|
|
}
|
|
// Otherwise, check if we should create a new guild
|
|
if (!guild) {
|
|
const guilds = await discord.snow.user.getGuilds({limit: 11, with_counts: false})
|
|
if (guilds.length < 10) {
|
|
console.log(" Creating a guild for emojis...")
|
|
guild = await discord.snow.guild.createGuild({name: "OOYE Emojis"})
|
|
}
|
|
}
|
|
// Otherwise, it's the user's problem
|
|
if (!guild) {
|
|
return die(`Error: The bot needs to upload some emojis. Please say where to upload them to. Run seed.js again with --emoji-guild=GUILD_ID`)
|
|
}
|
|
// Upload those emojis to the chosen location
|
|
db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)").run(guild.id)
|
|
await uploadAutoEmoji(discord.snow, guild, "L1", "docs/img/L1.png")
|
|
await uploadAutoEmoji(discord.snow, guild, "L2", "docs/img/L2.png")
|
|
}
|
|
console.log("✅ Emojis are ready...")
|
|
|
|
// set profile data on discord...
|
|
const avatarImageBuffer = await fetch("https://cadence.moe/friends/out_of_your_element.png").then(res => res.arrayBuffer())
|
|
await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + Buffer.from(avatarImageBuffer).toString("base64")})
|
|
await discord.snow.requestHandler.request(`/applications/@me`, {}, "patch", "json", {description: "Powered by **Out Of Your Element**\nhttps://gitdab.com/cadence/out-of-your-element"})
|
|
console.log("✅ Discord profile updated...")
|
|
|
|
// set profile data on homeserver...
|
|
await api.profileSetDisplayname(mxid, "Out Of Your Element")
|
|
await api.profileSetAvatarUrl(mxid, avatarUrl)
|
|
console.log("✅ Matrix profile updated...")
|
|
|
|
console.log("Good to go. I hope you enjoy Out Of Your Element.")
|
|
process.exit()
|
|
})()
|