forked from cadence/out-of-your-element
Emergency sync #11
8 changed files with 124 additions and 6 deletions
9
src/db/migrations/0035-role-default.sql
Normal file
9
src/db/migrations/0035-role-default.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "role_default" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"role_id" TEXT NOT NULL,
|
||||
PRIMARY KEY ("guild_id", "role_id")
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
5
src/db/orm-defs.d.ts
vendored
5
src/db/orm-defs.d.ts
vendored
|
|
@ -104,6 +104,11 @@ export type Models = {
|
|||
historical_room_index: number
|
||||
}
|
||||
|
||||
role_default: {
|
||||
guild_id: string
|
||||
role_id: string
|
||||
}
|
||||
|
||||
room_upgrade_pending: {
|
||||
new_room_id: string
|
||||
old_room_id: string
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ function renderPath(event, path, locals) {
|
|||
compile()
|
||||
fs.watch(path, {persistent: false}, compile)
|
||||
fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile)
|
||||
fs.watch(join(__dirname, "pug", "fragments"), {persistent: false}, compile)
|
||||
}
|
||||
|
||||
const cb = pugCache.get(path)
|
||||
|
|
|
|||
5
src/web/pug/fragments/default-roles-list.pug
Normal file
5
src/web/pug/fragments/default-roles-list.pug
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//- locals: guild, guild_id
|
||||
|
||||
include ../includes/default-roles-list.pug
|
||||
+default-roles-list(guild, guild_id)
|
||||
+add-roles-menu(guild, guild_id)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
extends includes/template.pug
|
||||
include includes/default-roles-list.pug
|
||||
|
||||
mixin badge-readonly
|
||||
.s-badge.s-badge__xs.s-badge__icon.s-badge__muted
|
||||
|
|
@ -76,7 +77,7 @@ block body
|
|||
|
||||
if space_id
|
||||
h2.mt48.fs-headline1 Server settings
|
||||
h3.mt32.fs-category Privacy level
|
||||
h3.mt32.fs-category How Matrix users join
|
||||
span#privacy-level-loading
|
||||
.s-card
|
||||
form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
||||
|
|
@ -105,6 +106,24 @@ block body
|
|||
p.s-description.m0 Shareable invite links, like Discord
|
||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
||||
|
||||
h3.mt32.fs-category Default roles
|
||||
.s-card
|
||||
form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles
|
||||
input(type="hidden" name="guild_id" value=guild_id)
|
||||
.d-flex.fw-wrap.g4
|
||||
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
||||
|
||||
+default-roles-list(guild, guild_id)
|
||||
|
||||
button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0
|
||||
.s-tag--dismiss.m1
|
||||
!= icons.Icons.IconPlusSm
|
||||
|
||||
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
||||
.s-popover--arrow.s-popover--arrow__tc
|
||||
+add-roles-menu(guild, guild_id)
|
||||
p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
|
||||
|
||||
h3.mt32.fs-category Features
|
||||
.s-card.d-grid.px0.g16
|
||||
form.d-flex.ai-center.g16
|
||||
|
|
|
|||
19
src/web/pug/includes/default-roles-list.pug
Normal file
19
src/web/pug/includes/default-roles-list.pug
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
mixin default-roles-list(guild, guild_id)
|
||||
#default-roles-list(style="display: contents")
|
||||
each roleID in select("role_default", "role_id", {guild_id}).pluck().all()
|
||||
- let r = guild.roles.find(r => r.id === roleID)
|
||||
if r
|
||||
.s-tag.s-tag__md.fs-body1= r.name
|
||||
span(id=`role-loading-${roleID}`)
|
||||
button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss
|
||||
!= icons.Icons.IconClearSm
|
||||
|
||||
mixin add-roles-menu(guild, guild_id)
|
||||
ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu
|
||||
li.s-menu--title.d-flex(role="separator") Select role
|
||||
span#add-role-loading
|
||||
each r in guild.roles.sort((a, b) => b.position - a.position)
|
||||
if r.id !== guild_id && !r.managed
|
||||
- let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get()
|
||||
li(role="menuitem")
|
||||
button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name
|
||||
|
|
@ -91,6 +91,19 @@ html(lang="en")
|
|||
.s-btn__dropdown:has(+ :popover-open) {
|
||||
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
|
||||
}
|
||||
.s-btn__dropdown.s-tag:has(+ :popover-open) .s-tag--dismiss {
|
||||
background-color: var(--black-500) !important;
|
||||
color: var(--black-150) !important;
|
||||
}
|
||||
.s-tag .is-loading {
|
||||
margin-right: -4px;
|
||||
}
|
||||
.s-tag .is-loading + .s-tag--dismiss {
|
||||
display: none !important;
|
||||
}
|
||||
a.s-block-link, .s-block-link {
|
||||
--_bl-bs-color: var(--green-400);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.theme-system .s-popover {
|
||||
--_po-bg: var(--black-100);
|
||||
|
|
@ -141,11 +154,15 @@ html(lang="en")
|
|||
//- Guild list popover
|
||||
script.
|
||||
document.querySelectorAll("[popovertarget]").forEach(e => {
|
||||
e.addEventListener("click", () => {
|
||||
const rect = e.getBoundingClientRect()
|
||||
const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
|
||||
const target = document.getElementById(e.getAttribute("popovertarget"))
|
||||
e.addEventListener("click", calculate)
|
||||
target.addEventListener("toggle", calculate)
|
||||
function calculate() {
|
||||
const buttonRect = e.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const t = `:popover-open { position: absolute; top: ${Math.floor(buttonRect.bottom + window.scrollY)}px; left: ${Math.floor(Math.max(targetRect.width / 2, buttonRect.left + buttonRect.width / 2))}px; width: ${Math.floor(buttonRect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
|
||||
document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length)
|
||||
})
|
||||
}
|
||||
})
|
||||
//- Prevent default
|
||||
script.
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ const assert = require("assert/strict")
|
|||
const {z} = require("zod")
|
||||
const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3")
|
||||
|
||||
const {as, db, sync, select} = require("../../passthrough")
|
||||
const {as, db, sync, select, discord} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../auth")} */
|
||||
const auth = sync.require("../auth")
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
/** @type {import("../../d2m/actions/set-presence")} */
|
||||
const setPresence = sync.require("../../d2m/actions/set-presence")
|
||||
|
||||
|
|
@ -20,6 +22,14 @@ function getCreateSpace(event) {
|
|||
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
|
||||
}
|
||||
|
||||
const schema = {
|
||||
defaultRoles: z.object({
|
||||
guild_id: z.string(),
|
||||
toggle_role: z.string().optional(),
|
||||
remove_role: z.string().optional()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef Options
|
||||
* @prop {(value: string?) => number} transform
|
||||
|
|
@ -94,3 +104,36 @@ as.router.post("/api/privacy-level", defineToggle("privacy_level", {
|
|||
await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
|
||||
}
|
||||
}))
|
||||
|
||||
as.router.post("/api/default-roles", defineEventHandler(async event => {
|
||||
const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse)
|
||||
|
||||
const managed = await auth.getManagedGuilds(event)
|
||||
const guildID = parsedBody.guild_id
|
||||
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||
|
||||
const roleID = parsedBody.toggle_role || parsedBody.remove_role
|
||||
assert(roleID)
|
||||
assert.notEqual(guildID, roleID) // the @everyone role is always default
|
||||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
|
||||
let shouldRemove = !!parsedBody.remove_role
|
||||
if (!shouldRemove) {
|
||||
shouldRemove = !!select("role_default", "role_id", {guild_id: guildID, role_id: roleID}).get()
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
db.prepare("DELETE FROM role_default WHERE guild_id = ? AND role_id = ?").run(guildID, roleID)
|
||||
} else {
|
||||
assert(guild.roles.find(r => r.id === roleID))
|
||||
db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID)
|
||||
}
|
||||
|
||||
if (getRequestHeader(event, "HX-Request")) {
|
||||
return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID})
|
||||
} else {
|
||||
return sendRedirect(event, "", 302)
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue