Allow for custom additions to webroot

This commit is contained in:
Cadence Ember
2026-02-17 12:54:50 +13:00
parent e779b41072
commit 0cd7e1c336
8 changed files with 127 additions and 40 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ registration.yaml
ooye.db*
events.db*
backfill.db*
custom-webroot
# Automatically generated
node_modules

View File

@@ -89,7 +89,7 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin
# Dependency justification
Total transitive production dependencies: 137
Total transitive production dependencies: 134
### <font size="+2">🦕</font>
@@ -119,8 +119,8 @@ Total transitive production dependencies: 137
* (0) entities: Looks fine. No dependencies.
* (0) get-relative-path: Looks fine. No dependencies.
* (1) heatsync: Module hot-reloader that I trust.
* (1) js-yaml: Will be removed in the future after registration.yaml is converted to JSON.
* (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used.
* (0) mime-type: File extension to mime type mapping that's already pulled in by stream-mime-type.
* (0) prettier-bytes: It does what I want and has no dependencies.
* (0) snowtransfer: Discord API library with bring-your-own-caching that I trust.
* (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well.

2
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"heatsync": "^2.7.2",
"htmx.org": "^2.0.4",
"lru-cache": "^11.0.2",
"mime-types": "^2.1.35",
"prettier-bytes": "^1.0.4",
"sharp": "^0.34.5",
"snowtransfer": "^0.17.1",
@@ -2073,6 +2074,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},

View File

@@ -42,6 +42,7 @@
"heatsync": "^2.7.2",
"htmx.org": "^2.0.4",
"lru-cache": "^11.0.2",
"mime-types": "^2.1.35",
"prettier-bytes": "^1.0.4",
"sharp": "^0.34.5",
"snowtransfer": "^0.17.1",

View File

@@ -31,7 +31,15 @@ function addGlobals(obj) {
*/
function render(event, filename, locals) {
const path = join(__dirname, "pug", filename)
return renderPath(event, path, locals)
}
/**
* @param {import("h3").H3Event} event
* @param {string} path
* @param {Record<string, any>} locals
*/
function renderPath(event, path, locals) {
function compile() {
try {
const template = compileFile(path, {pretty})
@@ -89,4 +97,5 @@ function createRoute(router, url, filename) {
module.exports.addGlobals = addGlobals
module.exports.render = render
module.exports.renderPath = renderPath
module.exports.createRoute = createRoute

View File

@@ -41,16 +41,18 @@ block body
= ` Set up self-service`
.s-prose
h2 What is this?
p #[a(href="https://gitdab.com/cadence/out-of-your-element") Out Of Your Element] is a bridge between the Discord and Matrix chat apps. It lets people on both platforms chat with each other without needing to get everyone on the same app.
p Just chat like usual, and the bridge will forward messages back and forth between the two platforms, so everyone sees the whole conversation.
p All kinds of content are supported, including pictures, threads, emojis, and @mentions.
p It's really easy to set up, even if you only have Discord. Just add the bot to your server, and it'll make everything available on Matrix automatically.
block bridge-info
h2 What is this?
p #[a(href="https://gitdab.com/cadence/out-of-your-element") Out Of Your Element] is a bridge between the Discord and Matrix chat apps. It lets people on both platforms chat with each other without needing to get everyone on the same app.
p Just chat like usual, and the bridge will forward messages back and forth between the two platforms, so everyone sees the whole conversation.
p All kinds of content are supported, including pictures, threads, emojis, and @mentions.
p It's really easy to set up, even if you only have Discord. Just add the bot to your server, and it'll make everything available on Matrix automatically.
if locked
h2 This is a private instance
p Anybody can run their own instance of the Out Of Your Element software. The person running this instance has made it private, so you can't add it to your server just yet. If you know who's in charge of #{reg.ooye.server_name}, ask them for the password.
block locked-info
h2 This is a private instance
p Anybody can run their own instance of the Out Of Your Element software. The person running this instance has made it private, so you can't add it to your server just yet. If you know who's in charge of #{reg.ooye.server_name}, ask them for the password.
h2 Run your own instance
p You can still use Out Of Your Element by running your own copy of the software, but this requires some technical skill.
p To get started, #[a(href="https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/get-started.md") check the installation instructions.]
h2 Run your own instance
p You can still use Out Of Your Element by running your own copy of the software, but this requires some technical skill.
p To get started, #[a(href="https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/get-started.md") check the installation instructions.]

View File

@@ -1,4 +1,10 @@
mixin guild(guild)
mixin guild-menuitem(guild)
- let bridgedRoomCount = from("channel_room").selectUnsafe("count(*) as count").where({guild_id: guild.id}).and("AND thread_parent IS NULL").get().count
li(role="menuitem")
a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`) class={"bg-purple-200": bridgedRoomCount === 0, "h:bg-purple-300": bridgedRoomCount === 0})
+guild(guild, bridgedRoomCount)
mixin guild(guild, bridgedRoomCount)
span.s-avatar.s-avatar__32.s-user-card--avatar
if guild.icon
img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32` alt="")
@@ -6,8 +12,12 @@ mixin guild(guild)
.s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0]
.s-user-card--info.ai-start
strong= guild.name
ul.s-user-card--awards
li #{discord.guildChannelMap.get(guild.id).filter(c => [0, 5, 15, 16].includes(discord.channels.get(c).type)).length} channels
if bridgedRoomCount != null
ul.s-user-card--awards
if bridgedRoomCount
li #{bridgedRoomCount} bridged rooms
else
li.fc-purple Not yet linked
mixin define-theme(name, h, s, l)
style.
@@ -58,6 +68,8 @@ html(lang="en")
title Out Of Your Element
<meta name="viewport" content="width=device-width, initial-scale=1">
link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css"))
//- Please use responsibly!!!!!
link(rel="stylesheet" type="text/css" href=rel("/custom.css"))
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 80%22><text y=%22.83em%22 font-size=%2283%22>💬</text></svg>">
meta(name="htmx-config" content='{"requestClass":"is-loading"}')
style.
@@ -79,6 +91,14 @@ html(lang="en")
.s-btn__dropdown:has(+ :popover-open) {
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
}
@media (prefers-color-scheme: dark) {
body.theme-system .s-popover {
--_po-bg: var(--black-100);
--_po-bc: var(--bc-light);
--_po-bs: var(--bs-lg);
--_po-arrow-fc: var(--black-100);
}
}
+define-themed-button("matrix", "black")
body.themed.theme-system
header.s-topbar
@@ -114,9 +134,7 @@ html(lang="en")
.s-popover--content.overflow-y-auto.overflow-x-hidden
ul.s-menu(role="menu")
each guild in [...managed].map(id => discord.guilds.get(id)).filter(g => g).sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)
li(role="menuitem")
a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`))
+guild(guild)
+guild-menuitem(guild)
//- Body
.mx-auto.w100.wmx9.py24.px8.fs-body1#content
block body

View File

@@ -4,13 +4,14 @@ const assert = require("assert")
const fs = require("fs")
const {join} = require("path")
const h3 = require("h3")
const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, handleCacheHeaders} = h3
const mimeTypes = require("mime-types")
const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, handleCacheHeaders, serveStatic} = h3
const icons = require("@stackoverflow/stacks-icons")
const DiscordTypes = require("discord-api-types/v10")
const dUtils = require("../discord/utils")
const reg = require("../matrix/read-registration")
const {sync, discord, as, select} = require("../passthrough")
const {sync, discord, as, select, from} = require("../passthrough")
/** @type {import("./pug-sync")} */
const pugSync = sync.require("./pug-sync")
/** @type {import("../matrix/utils")} */
@@ -19,21 +20,7 @@ const {id} = require("../../addbot")
// Pug
pugSync.addGlobals({id, h3, discord, select, DiscordTypes, dUtils, mUtils, icons, reg: reg.reg})
pugSync.createRoute(as.router, "/", "home.pug")
pugSync.createRoute(as.router, "/ok", "ok.pug")
// Routes
sync.require("./routes/download-matrix")
sync.require("./routes/download-discord")
sync.require("./routes/guild-settings")
sync.require("./routes/guild")
sync.require("./routes/info")
sync.require("./routes/link")
sync.require("./routes/log-in-with-matrix")
sync.require("./routes/oauth")
sync.require("./routes/password")
pugSync.addGlobals({id, h3, discord, select, from, DiscordTypes, dUtils, mUtils, icons, reg: reg.reg})
// Files
@@ -65,12 +52,79 @@ as.router.get("/static/htmx.js", defineEventHandler({
}
}))
as.router.get("/icon.png", defineEventHandler(event => {
handleCacheHeaders(event, {maxAge: 86400})
return fs.promises.readFile(join(__dirname, "../../docs/img/icon.png"))
}))
as.router.get("/download/file/poll-star-avatar.png", defineEventHandler(event => {
handleCacheHeaders(event, {maxAge: 86400})
return fs.promises.readFile(join(__dirname, "../../docs/img/poll-star-avatar.png"))
}))
// Custom files
const publicDir = "custom-webroot"
/**
* @param {h3.H3Event} event
* @param {boolean} fallthrough
*/
function tryStatic(event, fallthrough) {
return serveStatic(event, {
indexNames: ["/index.html", "/index.pug"],
fallthrough,
getMeta: async id => {
// Check
const stats = await fs.promises.stat(join(publicDir, id)).catch(() => {});
if (!stats || !stats.isFile()) {
return
}
// Pug
if (id.match(/\.pug$/)) {
defaultContentType(event, "text/html; charset=utf-8")
return {}
}
// Everything else
else {
const mime = mimeTypes.lookup(id)
if (typeof mime === "string") defaultContentType(event, mime)
return {
size: stats.size
}
}
},
getContents: id => {
if (id.match(/\.pug$/)) {
const path = join(publicDir, id)
return pugSync.renderPath(event, path, {})
} else {
return fs.promises.readFile(join(publicDir, id))
}
}
})
}
as.router.get("/**", defineEventHandler(event => {
return tryStatic(event, false)
}))
as.router.get("/", defineEventHandler(async event => {
return (await tryStatic(event, true)) || pugSync.render(event, "home.pug", {})
}))
as.router.get("/icon.png", defineEventHandler(async event => {
const s = await tryStatic(event, true)
if (s) return s
handleCacheHeaders(event, {maxAge: 86400})
return fs.promises.readFile(join(__dirname, "../../docs/img/icon.png"))
}))
// Routes
pugSync.createRoute(as.router, "/ok", "ok.pug")
sync.require("./routes/download-matrix")
sync.require("./routes/download-discord")
sync.require("./routes/guild-settings")
sync.require("./routes/guild")
sync.require("./routes/info")
sync.require("./routes/link")
sync.require("./routes/log-in-with-matrix")
sync.require("./routes/oauth")
sync.require("./routes/password")