Compare commits
4 commits
d4a50cb8aa
...
eb4aa615be
Author | SHA1 | Date | |
---|---|---|---|
eb4aa615be | |||
a459ee1d1c | |||
b1b9124052 | |||
5c0e830658 |
10 changed files with 5448 additions and 109 deletions
|
@ -45,7 +45,7 @@ Here are some tables that could potentially have foreign keys added between them
|
||||||
* The storage cost of the additional index on `sim` would not be worth the benefits.
|
* The storage cost of the additional index on `sim` would not be worth the benefits.
|
||||||
* `channel_room` <--(**C** room_id PK)-- `sim_member`
|
* `channel_room` <--(**C** room_id PK)-- `sim_member`
|
||||||
* If a room is being permanently unlinked, it may be useful to see a populated member list. If it's about to be relinked to another channel, we want to keep the sims in the room for more speed and to avoid spamming state events into the timeline.
|
* If a room is being permanently unlinked, it may be useful to see a populated member list. If it's about to be relinked to another channel, we want to keep the sims in the room for more speed and to avoid spamming state events into the timeline.
|
||||||
* Either way, the sims should remain in the room even after it's been unlinked. So no referential integrity is desirable here.
|
* Either way, the sims could remain in the room even after it's been unlinked. So no referential integrity is desirable here.
|
||||||
* `sim` <--(PK user_id PK)-- `sim_proxy`
|
* `sim` <--(PK user_id PK)-- `sim_proxy`
|
||||||
* OOYE left joins on this. In normal operation, this relationship might not exist.
|
* OOYE left joins on this. In normal operation, this relationship might not exist.
|
||||||
* `channel_room` <--(PK channel_id PK)-- `webhook` ✅
|
* `channel_room` <--(PK channel_id PK)-- `webhook` ✅
|
||||||
|
|
|
@ -38,4 +38,6 @@ passthrough.select = orm.select
|
||||||
await discord.cloud.connect()
|
await discord.cloud.connect()
|
||||||
console.log("Discord gateway started")
|
console.log("Discord gateway started")
|
||||||
sync.require("../src/web/server")
|
sync.require("../src/web/server")
|
||||||
|
|
||||||
|
require("../src/stdin")
|
||||||
})()
|
})()
|
||||||
|
|
|
@ -6,7 +6,7 @@ const Ty = require("../../types")
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select} = passthrough
|
const {discord, sync, db, select, from} = passthrough
|
||||||
/** @type {import("../../matrix/file")} */
|
/** @type {import("../../matrix/file")} */
|
||||||
const file = sync.require("../../matrix/file")
|
const file = sync.require("../../matrix/file")
|
||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
|
@ -14,7 +14,9 @@ const api = sync.require("../../matrix/api")
|
||||||
/** @type {import("../../matrix/kstate")} */
|
/** @type {import("../../matrix/kstate")} */
|
||||||
const ks = sync.require("../../matrix/kstate")
|
const ks = sync.require("../../matrix/kstate")
|
||||||
/** @type {import("../../discord/utils")} */
|
/** @type {import("../../discord/utils")} */
|
||||||
const utils = sync.require("../../discord/utils")
|
const dUtils = sync.require("../../discord/utils")
|
||||||
|
/** @type {import("../../m2d/converters/utils")} */
|
||||||
|
const mUtils = sync.require("../../m2d/converters/utils")
|
||||||
/** @type {import("./create-space")} */
|
/** @type {import("./create-space")} */
|
||||||
const createSpace = sync.require("./create-space")
|
const createSpace = sync.require("./create-space")
|
||||||
|
|
||||||
|
@ -114,8 +116,8 @@ async function channelToKState(channel, guild, di) {
|
||||||
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
||||||
}
|
}
|
||||||
|
|
||||||
const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites)
|
const everyonePermissions = dUtils.getPermissions([], guild.roles, undefined, channel.permission_overwrites)
|
||||||
const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"])
|
const everyoneCanMentionEveryone = dUtils.hasAllPermissions(everyonePermissions, ["MentionEveryone"])
|
||||||
|
|
||||||
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
|
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
|
||||||
const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})
|
const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})
|
||||||
|
@ -392,7 +394,7 @@ function syncRoom(channelID) {
|
||||||
return _syncRoom(channelID, true)
|
return _syncRoom(channelID, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _unbridgeRoom(channelID) {
|
async function unbridgeChannel(channelID) {
|
||||||
/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */
|
/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */
|
||||||
const channel = discord.channels.get(channelID)
|
const channel = discord.channels.get(channelID)
|
||||||
assert.ok(channel)
|
assert.ok(channel)
|
||||||
|
@ -407,12 +409,8 @@ async function _unbridgeRoom(channelID) {
|
||||||
async function unbridgeDeletedChannel(channel, guildID) {
|
async function unbridgeDeletedChannel(channel, guildID) {
|
||||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||||
assert.ok(roomID)
|
assert.ok(roomID)
|
||||||
const spaceID = select("guild_space", "space_id", {guild_id: guildID}).pluck().get()
|
const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").get()
|
||||||
assert.ok(spaceID)
|
assert.ok(row)
|
||||||
|
|
||||||
// remove room from being a space member
|
|
||||||
await api.sendState(roomID, "m.space.parent", spaceID, {})
|
|
||||||
await api.sendState(spaceID, "m.space.child", roomID, {})
|
|
||||||
|
|
||||||
// remove declaration that the room is bridged
|
// remove declaration that the room is bridged
|
||||||
await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {})
|
await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {})
|
||||||
|
@ -421,15 +419,6 @@ async function unbridgeDeletedChannel(channel, guildID) {
|
||||||
await api.sendState(roomID, "m.room.topic", "", {topic: channel.topic || ""})
|
await api.sendState(roomID, "m.room.topic", "", {topic: channel.topic || ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a notification in the room
|
|
||||||
await api.sendEvent(roomID, "m.room.message", {
|
|
||||||
msgtype: "m.notice",
|
|
||||||
body: "⚠️ This room was removed from the bridge."
|
|
||||||
})
|
|
||||||
|
|
||||||
// leave room
|
|
||||||
await api.leaveRoom(roomID)
|
|
||||||
|
|
||||||
// delete webhook on discord
|
// delete webhook on discord
|
||||||
const webhook = select("webhook", ["webhook_id", "webhook_token"], {channel_id: channel.id}).get()
|
const webhook = select("webhook", ["webhook_id", "webhook_token"], {channel_id: channel.id}).get()
|
||||||
if (webhook) {
|
if (webhook) {
|
||||||
|
@ -439,7 +428,48 @@ async function unbridgeDeletedChannel(channel, guildID) {
|
||||||
|
|
||||||
// delete room from database
|
// delete room from database
|
||||||
db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID)
|
db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID)
|
||||||
db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id)
|
db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) // cascades to most other tables, like messages
|
||||||
|
|
||||||
|
// demote admins in room
|
||||||
|
/** @type {Ty.Event.M_Power_Levels} */
|
||||||
|
const powerLevelContent = await api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||||
|
powerLevelContent.users ??= {}
|
||||||
|
const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||||
|
for (const mxid of Object.keys(powerLevelContent.users)) {
|
||||||
|
if (mUtils.eventSenderIsFromDiscord(mxid) && mxid !== bot) {
|
||||||
|
delete powerLevelContent.users[mxid]
|
||||||
|
await api.sendState(roomID, "m.room.power_levels", "", powerLevelContent, mxid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a notification in the room
|
||||||
|
await api.sendEvent(roomID, "m.room.message", {
|
||||||
|
msgtype: "m.notice",
|
||||||
|
body: "⚠️ This room was removed from the bridge."
|
||||||
|
})
|
||||||
|
|
||||||
|
// if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged
|
||||||
|
// (don't do this for self-service rooms, because they might continue to be used on Matrix or linked somewhere else later)
|
||||||
|
if (row.autocreate === 1) {
|
||||||
|
// remove room from being a space member
|
||||||
|
await api.sendState(roomID, "m.space.parent", row.space_id, {})
|
||||||
|
await api.sendState(row.space_id, "m.space.child", roomID, {})
|
||||||
|
|
||||||
|
// leave room
|
||||||
|
await api.leaveRoom(roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it is a self-service room, remove sim members
|
||||||
|
// (the room can be used with less clutter and the member list makes sense if it's bridged somewhere else)
|
||||||
|
if (row.autocreate === 0) {
|
||||||
|
// remove sim members
|
||||||
|
const members = select("sim_member", "mxid", {room_id: roomID}).pluck().all()
|
||||||
|
const preparedDelete = db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?")
|
||||||
|
for (const mxid of members) {
|
||||||
|
await api.leaveRoom(roomID, mxid)
|
||||||
|
preparedDelete.run(roomID, mxid)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -488,7 +518,7 @@ module.exports.createAllForGuild = createAllForGuild
|
||||||
module.exports.channelToKState = channelToKState
|
module.exports.channelToKState = channelToKState
|
||||||
module.exports.postApplyPowerLevels = postApplyPowerLevels
|
module.exports.postApplyPowerLevels = postApplyPowerLevels
|
||||||
module.exports._convertNameAndTopic = convertNameAndTopic
|
module.exports._convertNameAndTopic = convertNameAndTopic
|
||||||
module.exports._unbridgeRoom = _unbridgeRoom
|
module.exports.unbridgeChannel = unbridgeChannel
|
||||||
module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel
|
module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel
|
||||||
module.exports.existsOrAutocreatable = existsOrAutocreatable
|
module.exports.existsOrAutocreatable = existsOrAutocreatable
|
||||||
module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable
|
module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable
|
||||||
|
|
|
@ -67,7 +67,7 @@ block body
|
||||||
option(value="admin") Admin
|
option(value="admin") Admin
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
.grid--row-start2
|
.grid--row-start2
|
||||||
button.s-btn.s-btn__filled.htmx-indicator Invite
|
button.s-btn.s-btn__filled Invite
|
||||||
div
|
div
|
||||||
!= svg
|
!= svg
|
||||||
|
|
||||||
|
@ -78,12 +78,13 @@ block body
|
||||||
h3.mt32.fs-category Linked channels
|
h3.mt32.fs-category Linked channels
|
||||||
|
|
||||||
.s-card.bs-sm.p0
|
.s-card.bs-sm.p0
|
||||||
.s-table-container
|
form.s-table-container(method="post" action="/api/unlink" hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources.")
|
||||||
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
table.s-table.s-table__bx-simple
|
table.s-table.s-table__bx-simple
|
||||||
each row in linkedChannelsWithDetails
|
each row in linkedChannelsWithDetails
|
||||||
tr
|
tr
|
||||||
td.w40: +discord(row.channel)
|
td.w40: +discord(row.channel)
|
||||||
td.p2: button.s-btn.s-btn__muted.s-btn__xs!= icons.Icons.IconLinkSm
|
td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" value=row.channel.id hx-post="/api/unlink" hx-trigger="click" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
|
||||||
td: +matrix(row)
|
td: +matrix(row)
|
||||||
else
|
else
|
||||||
tr
|
tr
|
||||||
|
@ -99,16 +100,16 @@ block body
|
||||||
- let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get()
|
- let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get()
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value)
|
input.s-toggle-switch.order-last#autocreate(name="autocreate" type="checkbox" hx-post="/api/autocreate" hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value)
|
||||||
.is-loading#autocreate-loading
|
#autocreate-loading
|
||||||
|
|
||||||
h3.mt32.fs-category Privacy level
|
h3.mt32.fs-category Privacy level
|
||||||
.s-card
|
.s-card
|
||||||
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="this")
|
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
.d-flex.ai-center.mb4
|
.d-flex.ai-center.mb4
|
||||||
label.s-label.fl-grow1
|
label.s-label.fl-grow1
|
||||||
| How people can join on Matrix
|
| How people can join on Matrix
|
||||||
span.is-loading#privacy-level-loading
|
span#privacy-level-loading
|
||||||
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr")
|
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr")
|
||||||
input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2))
|
input(type="radio" name="level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2))
|
||||||
label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory")
|
label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory")
|
||||||
|
@ -133,23 +134,23 @@ block body
|
||||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
||||||
|
|
||||||
h3.mt32.fs-category Manually link channels
|
h3.mt32.fs-category Manually link channels
|
||||||
form.d-flex.g16.ai-start(hx-post="/api/link" hx-trigger="submit" hx-disabled-elt="this")
|
form.d-flex.g16.ai-start(hx-post="/api/link" hx-trigger="submit" hx-disabled-elt="input, button" hx-indicator="#link-button")
|
||||||
.fl-grow2.s-btn-group.fd-column.w40
|
.fl-grow2.s-btn-group.fd-column.w40
|
||||||
each channel in unlinkedChannels
|
each channel in unlinkedChannels
|
||||||
input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id)
|
input.s-btn--radio(type="radio" name="discord" required id=channel.id value=channel.id)
|
||||||
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
||||||
+discord(channel, true, "Announcement")
|
+discord(channel, true, "Announcement")
|
||||||
else
|
else
|
||||||
.s-empty-state.p8 All Discord channels are linked.
|
.s-empty-state.p8 All Discord channels are linked.
|
||||||
.fl-grow1.s-btn-group.fd-column.w30
|
.fl-grow1.s-btn-group.fd-column.w30
|
||||||
each room in unlinkedRooms
|
each room in unlinkedRooms
|
||||||
input.s-btn--radio(type="radio" name="matrix" id=room.room_id value=room.room_id)
|
input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id)
|
||||||
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
|
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
|
||||||
+matrix(room, true)
|
+matrix(room, true)
|
||||||
else
|
else
|
||||||
.s-empty-state.p8 All Matrix rooms are linked.
|
.s-empty-state.p8 All Matrix rooms are linked.
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
div
|
div
|
||||||
button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator
|
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
||||||
!= icons.Icons.IconMerge
|
!= icons.Icons.IconMerge
|
||||||
= ` Link`
|
= ` Link`
|
||||||
|
|
|
@ -1,71 +1,89 @@
|
||||||
mixin guild(guild)
|
mixin guild(guild)
|
||||||
span.s-avatar.s-avatar__32.s-user-card--avatar
|
span.s-avatar.s-avatar__32.s-user-card--avatar
|
||||||
if guild.icon
|
if guild.icon
|
||||||
img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32`)
|
img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32`)
|
||||||
else
|
else
|
||||||
.s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0]
|
.s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0]
|
||||||
.s-user-card--info.ai-start
|
.s-user-card--info.ai-start
|
||||||
strong= guild.name
|
strong= guild.name
|
||||||
ul.s-user-card--awards
|
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
|
li #{discord.guildChannelMap.get(guild.id).filter(c => [0, 5, 15, 16].includes(discord.channels.get(c).type)).length} channels
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
html(lang="en")
|
html(lang="en")
|
||||||
head
|
head
|
||||||
title Out Of Your Element
|
title Out Of Your Element
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css"))
|
link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.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>">
|
<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='{"indicatorClass":"is-loading"}')
|
meta(name="htmx-config" content='{"requestClass":"is-loading"}')
|
||||||
style.
|
style.
|
||||||
.themed {
|
.themed {
|
||||||
--theme-base-primary-color-h: 266;
|
--theme-base-primary-color-h: 266;
|
||||||
--theme-base-primary-color-s: 53%;
|
--theme-base-primary-color-s: 53%;
|
||||||
--theme-base-primary-color-l: 63%;
|
--theme-base-primary-color-l: 63%;
|
||||||
--theme-dark-primary-color-h: 266;
|
--theme-dark-primary-color-h: 266;
|
||||||
--theme-dark-primary-color-s: 53%;
|
--theme-dark-primary-color-s: 53%;
|
||||||
--theme-dark-primary-color-l: 63%;
|
--theme-dark-primary-color-l: 63%;
|
||||||
}
|
}
|
||||||
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental input[type="radio"]:checked ~ label:not(.s-toggle-switch--label-off) {
|
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental input[type="radio"]:checked ~ label:not(.s-toggle-switch--label-off) {
|
||||||
--_ts-multiple-bg: var(--green-400);
|
--_ts-multiple-bg: var(--green-400);
|
||||||
--_ts-multiple-fc: var(--white);
|
--_ts-multiple-fc: var(--white);
|
||||||
}
|
}
|
||||||
body.themed.theme-system
|
body.themed.theme-system
|
||||||
header.s-topbar
|
header.s-topbar
|
||||||
.s-topbar--skip-link(href="#content") Skip to main content
|
.s-topbar--skip-link(href="#content") Skip to main content
|
||||||
.s-topbar--container.wmx9
|
.s-topbar--container.wmx9
|
||||||
a.s-topbar--logo(href=rel("/"))
|
a.s-topbar--logo(href=rel("/"))
|
||||||
img.s-avatar.s-avatar__32(src=rel("/icon.png"))
|
img.s-avatar.s-avatar__32(src=rel("/icon.png"))
|
||||||
nav.s-topbar--navigation
|
nav.s-topbar--navigation
|
||||||
ul.s-topbar--content
|
ul.s-topbar--content
|
||||||
li.ps-relative
|
li.ps-relative
|
||||||
if !session.data.managedGuilds || session.data.managedGuilds.length === 0
|
if !session.data.managedGuilds || session.data.managedGuilds.length === 0
|
||||||
a.s-btn.s-btn__icon.as-center(href=rel("/oauth"))
|
a.s-btn.s-btn__icon.as-center(href=rel("/oauth"))
|
||||||
!= icons.Icons.IconDiscord
|
!= icons.Icons.IconDiscord
|
||||||
= ` Log in`
|
= ` Log in`
|
||||||
else if guild_id && session.data.managedGuilds.includes(guild_id) && discord.guilds.has(guild_id)
|
else if guild_id && session.data.managedGuilds.includes(guild_id) && discord.guilds.has(guild_id)
|
||||||
button.s-topbar--item.s-btn.s-btn__muted.s-user-card(popovertarget="guilds")
|
button.s-topbar--item.s-btn.s-btn__muted.s-user-card(popovertarget="guilds")
|
||||||
+guild(discord.guilds.get(guild_id))
|
+guild(discord.guilds.get(guild_id))
|
||||||
else if session.data.managedGuilds
|
else if session.data.managedGuilds
|
||||||
button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.s-label(popovertarget="guilds")
|
button.s-topbar--item.s-btn.s-btn__muted.s-btn__dropdown.pr24.s-user-card.s-label(popovertarget="guilds")
|
||||||
| Your servers
|
| Your servers
|
||||||
#guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible
|
#guilds(popover data-popper-placement="bottom" style="display: revert; width: revert;").s-popover.overflow-visible
|
||||||
.s-popover--arrow.s-popover--arrow__tc
|
.s-popover--arrow.s-popover--arrow__tc
|
||||||
.s-popover--content.overflow-y-auto.overflow-x-hidden
|
.s-popover--content.overflow-y-auto.overflow-x-hidden
|
||||||
ul.s-menu(role="menu")
|
ul.s-menu(role="menu")
|
||||||
each guild in (session.data.managedGuilds || []).map(id => discord.guilds.get(id)).filter(g => g)
|
each guild in (session.data.managedGuilds || []).map(id => discord.guilds.get(id)).filter(g => g)
|
||||||
li(role="menuitem")
|
li(role="menuitem")
|
||||||
a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`))
|
a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`))
|
||||||
+guild(guild)
|
+guild(guild)
|
||||||
.mx-auto.w100.wmx9.py24.px8.fs-body1#content
|
//- Body
|
||||||
block body
|
.mx-auto.w100.wmx9.py24.px8.fs-body1#content
|
||||||
script.
|
block body
|
||||||
document.querySelectorAll("[popovertarget]").forEach(e => {
|
//- Guild list popover
|
||||||
e.addEventListener("click", () => {
|
script.
|
||||||
const rect = e.getBoundingClientRect()
|
document.querySelectorAll("[popovertarget]").forEach(e => {
|
||||||
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 }`
|
e.addEventListener("click", () => {
|
||||||
// console.log(t)
|
const rect = e.getBoundingClientRect()
|
||||||
document.styleSheets[0].insertRule(t)
|
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 }`
|
||||||
})
|
// console.log(t)
|
||||||
})
|
document.styleSheets[0].insertRule(t)
|
||||||
script(src=rel("/static/htmx.min.js"))
|
})
|
||||||
|
})
|
||||||
|
script(src=rel("/static/htmx.js"))
|
||||||
|
//- Error dialog
|
||||||
|
aside.s-modal#server-error(aria-hidden="true")
|
||||||
|
.s-modal--dialog
|
||||||
|
h1.s-modal--header Server error
|
||||||
|
pre.overflow-auto#server-error-content
|
||||||
|
button.s-modal--close.s-btn.s-btn__muted(aria-label="Close" type="button" onclick="hideError()")!= icons.Icons.IconClearSm
|
||||||
|
.s-modal--footer
|
||||||
|
button.s-btn.s-btn__outlined.s-btn__muted(type="button" onclick="hideError()") OK
|
||||||
|
script.
|
||||||
|
function hideError() {
|
||||||
|
document.getElementById("server-error").setAttribute("aria-hidden", "true")
|
||||||
|
}
|
||||||
|
document.body.addEventListener("htmx:responseError", event => {
|
||||||
|
document.getElementById("server-error").setAttribute("aria-hidden", "false")
|
||||||
|
document.getElementById("server-error-content").textContent = event.detail.xhr.responseText
|
||||||
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const assert = require("assert/strict")
|
const assert = require("assert/strict")
|
||||||
const {z} = require("zod")
|
const {z} = require("zod")
|
||||||
const {defineEventHandler, sendRedirect, useSession, createError, readValidatedBody} = require("h3")
|
const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3")
|
||||||
|
|
||||||
const {as, db, sync} = require("../../passthrough")
|
const {as, db, sync} = require("../../passthrough")
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const {z} = require("zod")
|
const {z} = require("zod")
|
||||||
const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3")
|
const {defineEventHandler, useSession, createError, readValidatedBody, setResponseHeader} = require("h3")
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
|
||||||
const {discord, db, as, sync, select, from} = require("../../passthrough")
|
const {discord, db, as, sync, select, from} = require("../../passthrough")
|
||||||
|
@ -19,6 +19,10 @@ const schema = {
|
||||||
guild_id: z.string(),
|
guild_id: z.string(),
|
||||||
matrix: z.string(),
|
matrix: z.string(),
|
||||||
discord: z.string()
|
discord: z.string()
|
||||||
|
}),
|
||||||
|
unlink: z.object({
|
||||||
|
guild_id: z.string(),
|
||||||
|
channel_id: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,5 +63,29 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
// Sync room data and space child
|
// Sync room data and space child
|
||||||
await createRoom.syncRoom(parsedBody.discord)
|
await createRoom.syncRoom(parsedBody.discord)
|
||||||
|
|
||||||
|
setResponseHeader(event, "HX-Refresh", "true")
|
||||||
|
return null // 204
|
||||||
|
}))
|
||||||
|
|
||||||
|
as.router.post("/api/unlink", defineEventHandler(async event => {
|
||||||
|
const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
|
||||||
|
const session = await useSession(event, {password: reg.as_token})
|
||||||
|
|
||||||
|
// Check guild ID or nonce
|
||||||
|
if (!(session.data.managedGuilds || []).includes(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
||||||
|
|
||||||
|
// Check channel is part of this guild
|
||||||
|
const channel = discord.channels.get(channel_id)
|
||||||
|
if (!channel) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} does not exist`})
|
||||||
|
if (!("guild_id" in channel) || channel.guild_id !== guild_id) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not part of guild ${guild_id}`})
|
||||||
|
|
||||||
|
// Check channel is currently bridged
|
||||||
|
const row = select("channel_room", "channel_id", {channel_id: channel_id}).get()
|
||||||
|
if (!row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not currently bridged`})
|
||||||
|
|
||||||
|
// Do it
|
||||||
|
await createRoom.unbridgeDeletedChannel(channel, guild_id)
|
||||||
|
|
||||||
|
setResponseHeader(event, "HX-Refresh", "true")
|
||||||
return null // 204
|
return null // 204
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -49,12 +49,12 @@ as.router.get("/static/stacks.min.css", defineEventHandler({
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
as.router.get("/static/htmx.min.js", defineEventHandler({
|
as.router.get("/static/htmx.js", defineEventHandler({
|
||||||
onBeforeResponse: compressResponse,
|
onBeforeResponse: compressResponse,
|
||||||
handler: async event => {
|
handler: async event => {
|
||||||
handleCacheHeaders(event, {maxAge: 86400})
|
handleCacheHeaders(event, {maxAge: 86400})
|
||||||
defaultContentType(event, "text/javascript")
|
defaultContentType(event, "text/javascript")
|
||||||
return fs.promises.readFile(join(__dirname, "static", "htmx.min.js"), "utf-8")
|
return fs.promises.readFile(join(__dirname, "static", "htmx.js"), "utf-8")
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
5261
src/web/static/htmx.js
Normal file
5261
src/web/static/htmx.js
Normal file
File diff suppressed because it is too large
Load diff
1
src/web/static/htmx.min.js
vendored
1
src/web/static/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue