UI for linking existing space
This commit is contained in:
@@ -69,3 +69,9 @@ So here's all the technical changes needed to support self-service in v3:
|
|||||||
- When bot is added through "self-service" web button, REPLACE INTO state 0.
|
- When bot is added through "self-service" web button, REPLACE INTO state 0.
|
||||||
- Event dispatcher will only ensureRoom if the guild_active state is 1.
|
- Event dispatcher will only ensureRoom if the guild_active state is 1.
|
||||||
- createRoom will only create other dependencies if the guild is autocreate.
|
- createRoom will only create other dependencies if the guild is autocreate.
|
||||||
|
|
||||||
|
## Enough with your theory. How do rooms actually get bridged now?
|
||||||
|
|
||||||
|
After clicking the easy mode button on web and adding the bot to a server, it will create new Matrix rooms on-demand when any invite features are used (web or command) OR just when any message is sent on Discord.
|
||||||
|
|
||||||
|
Alternatively, pressing the self-service mode button and adding the bot to a server will prompt the web user to link it with a space. After doing so, they'll be on the standard guild management page where they can invite to the space and manually link rooms. Nothing will be autocreated.
|
||||||
|
@@ -4,6 +4,9 @@ CREATE TABLE "invite" (
|
|||||||
"mxid" TEXT NOT NULL,
|
"mxid" TEXT NOT NULL,
|
||||||
"room_id" TEXT NOT NULL,
|
"room_id" TEXT NOT NULL,
|
||||||
"type" TEXT,
|
"type" TEXT,
|
||||||
|
"name" TEXT,
|
||||||
|
"topic" TEXT,
|
||||||
|
"avatar" TEXT,
|
||||||
PRIMARY KEY("mxid","room_id")
|
PRIMARY KEY("mxid","room_id")
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
|
2
src/db/orm-defs.d.ts
vendored
2
src/db/orm-defs.d.ts
vendored
@@ -44,6 +44,8 @@ export type Models = {
|
|||||||
mxid: string
|
mxid: string
|
||||||
room_id: string
|
room_id: string
|
||||||
type: string | null
|
type: string | null
|
||||||
|
name: string | null
|
||||||
|
avatar: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
lottie: {
|
lottie: {
|
||||||
|
@@ -64,7 +64,9 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}])
|
}]).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
|
||||||
async function dispatchInteraction(interaction) {
|
async function dispatchInteraction(interaction) {
|
||||||
const interactionId = interaction.data.custom_id || interaction.data.name
|
const interactionId = interaction.data.custom_id || interaction.data.name
|
||||||
|
@@ -208,6 +208,26 @@ async event => {
|
|||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
function getFromInviteRoomState(inviteRoomState, nskey, key) {
|
||||||
|
if (!Array.isArray(inviteRoomState)) return null
|
||||||
|
for (const event of inviteRoomState) {
|
||||||
|
if (event.type === nskey && event.state_key === "") {
|
||||||
|
return event.content[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
sync.addTemporaryListener(as, "type:m.space.child", guard("m.space.child",
|
||||||
|
/**
|
||||||
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Space_Child>} event
|
||||||
|
*/
|
||||||
|
async event => {
|
||||||
|
if (Array.isArray(event.content.via) && event.content.via.length) { // space child is being added
|
||||||
|
await api.joinRoom(event.state_key).catch(() => {}) // try to join if able, it's okay if it doesn't want, bot will still respond to invites
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
|
sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Member>} event
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Member>} event
|
||||||
@@ -217,9 +237,14 @@ async event => {
|
|||||||
|
|
||||||
if (event.content.membership === "invite" && event.state_key === `@${reg.sender_localpart}:${reg.ooye.server_name}`) {
|
if (event.content.membership === "invite" && event.state_key === `@${reg.sender_localpart}:${reg.ooye.server_name}`) {
|
||||||
// We were invited to a room. We should join, and register the invite details for future reference in web.
|
// We were invited to a room. We should join, and register the invite details for future reference in web.
|
||||||
|
const name = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.name", "name")
|
||||||
|
const topic = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.topic", "topic")
|
||||||
|
const avatar = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.avatar", "url")
|
||||||
|
const creationType = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.create", "type")
|
||||||
|
if (!name) return await api.leaveRoomWithReason(event.room_id, "Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite!")
|
||||||
await api.joinRoom(event.room_id)
|
await api.joinRoom(event.room_id)
|
||||||
const creation = await api.getStateEvent(event.room_id, "m.room.create", "")
|
db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, creationType, name, topic, avatar)
|
||||||
db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type) VALUES (?, ?, ?)").run(event.sender, event.room_id, creation.type || null)
|
if (avatar) utils.getPublicUrlForMxc(avatar) // make sure it's available in the media_proxy allowed URLs
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.eventSenderIsFromDiscord(event.state_key)) return
|
if (utils.eventSenderIsFromDiscord(event.state_key)) return
|
||||||
|
@@ -82,6 +82,16 @@ async function leaveRoom(roomID, mxid) {
|
|||||||
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {})
|
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} roomID
|
||||||
|
* @param {string} reason
|
||||||
|
* @param {string} [mxid]
|
||||||
|
*/
|
||||||
|
async function leaveRoomWithReason(roomID, reason, mxid) {
|
||||||
|
console.log(`[api] leave: ${roomID}: ${mxid}, because ${reason}`)
|
||||||
|
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {reason})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
* @param {string} eventID
|
* @param {string} eventID
|
||||||
@@ -404,6 +414,7 @@ module.exports.createRoom = createRoom
|
|||||||
module.exports.joinRoom = joinRoom
|
module.exports.joinRoom = joinRoom
|
||||||
module.exports.inviteToRoom = inviteToRoom
|
module.exports.inviteToRoom = inviteToRoom
|
||||||
module.exports.leaveRoom = leaveRoom
|
module.exports.leaveRoom = leaveRoom
|
||||||
|
module.exports.leaveRoomWithReason = leaveRoomWithReason
|
||||||
module.exports.getEvent = getEvent
|
module.exports.getEvent = getEvent
|
||||||
module.exports.getEventForTimestamp = getEventForTimestamp
|
module.exports.getEventForTimestamp = getEventForTimestamp
|
||||||
module.exports.getAllState = getAllState
|
module.exports.getAllState = getAllState
|
||||||
|
5
src/types.d.ts
vendored
5
src/types.d.ts
vendored
@@ -281,6 +281,11 @@ export namespace Event {
|
|||||||
users_default?: number
|
users_default?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type M_Space_Child = {
|
||||||
|
via?: string[]
|
||||||
|
suggested?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type M_Reaction = {
|
export type M_Reaction = {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
rel_type: "m.annotation"
|
rel_type: "m.annotation"
|
||||||
|
@@ -71,23 +71,24 @@ block body
|
|||||||
div
|
div
|
||||||
!= svg
|
!= svg
|
||||||
|
|
||||||
h2.mt48.fs-headline1 Matrix setup
|
if space_id
|
||||||
|
h2.mt48.fs-headline1 Matrix setup
|
||||||
|
|
||||||
h3.mt32.fs-category Linked channels
|
h3.mt32.fs-category Linked channels
|
||||||
|
|
||||||
.s-card.bs-sm.p0
|
.s-card.bs-sm.p0
|
||||||
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.")
|
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)
|
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(name="channel_id" value=row.channel.id hx-post="/api/unlink" hx-trigger="click" hx-disabled-elt="this")!= 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
|
||||||
td(colspan="3")
|
td(colspan="3")
|
||||||
.s-empty-state No channels linked between Discord and Matrix yet...
|
.s-empty-state No channels linked between Discord and Matrix yet...
|
||||||
|
|
||||||
h3.mt32.fs-category Auto-create
|
h3.mt32.fs-category Auto-create
|
||||||
.s-card
|
.s-card
|
||||||
@@ -97,95 +98,96 @@ block body
|
|||||||
p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in.
|
p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in.
|
||||||
- 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 autocomplete="off")
|
||||||
#autocreate-loading
|
#autocreate-loading
|
||||||
|
|
||||||
h3.mt32.fs-category Privacy level
|
if space_id
|
||||||
.s-card
|
h3.mt32.fs-category Privacy level
|
||||||
form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
.s-card
|
||||||
|
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)
|
||||||
|
.d-flex.ai-center.mb4
|
||||||
|
label.s-label.fl-grow1
|
||||||
|
| How people can join on Matrix
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
!= icons.Icons.IconPlusSm
|
||||||
|
!= icons.Icons.IconInternationalSm
|
||||||
|
.fl-grow1 Directory
|
||||||
|
|
||||||
|
input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1))
|
||||||
|
label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link")
|
||||||
|
!= icons.Icons.IconPlusSm
|
||||||
|
!= icons.Icons.IconLinkSm
|
||||||
|
.fl-grow1 Link
|
||||||
|
|
||||||
|
input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0))
|
||||||
|
label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite")
|
||||||
|
svg.svg-icon(width="14" height="14" viewBox="0 0 14 14")
|
||||||
|
!= icons.Icons.IconLockSm
|
||||||
|
.fl-grow1 Invite
|
||||||
|
|
||||||
|
p.s-description.m0 In-app direct invite from another user
|
||||||
|
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 Manually link channels
|
||||||
|
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
|
||||||
|
each channel in unlinkedChannels
|
||||||
|
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)
|
||||||
|
+discord(channel, true, "Announcement")
|
||||||
|
else
|
||||||
|
.s-empty-state.p8 All Discord channels are linked.
|
||||||
|
.fl-grow1.s-btn-group.fd-column.w30
|
||||||
|
each room in unlinkedRooms
|
||||||
|
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)
|
||||||
|
+matrix(room, true)
|
||||||
|
else
|
||||||
|
.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)
|
||||||
.d-flex.ai-center.mb4
|
|
||||||
label.s-label.fl-grow1
|
|
||||||
| How people can join on Matrix
|
|
||||||
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")
|
|
||||||
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")
|
|
||||||
!= icons.Icons.IconPlusSm
|
|
||||||
!= icons.Icons.IconInternationalSm
|
|
||||||
.fl-grow1 Directory
|
|
||||||
|
|
||||||
input(type="radio" name="level" value="link" id="privacy-level-link" checked=(privacy_level === 1))
|
|
||||||
label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link")
|
|
||||||
!= icons.Icons.IconPlusSm
|
|
||||||
!= icons.Icons.IconLinkSm
|
|
||||||
.fl-grow1 Link
|
|
||||||
|
|
||||||
input(type="radio" name="level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0))
|
|
||||||
label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite")
|
|
||||||
svg.svg-icon(width="14" height="14" viewBox="0 0 14 14")
|
|
||||||
!= icons.Icons.IconLockSm
|
|
||||||
.fl-grow1 Invite
|
|
||||||
|
|
||||||
p.s-description.m0 In-app direct invite from another user
|
|
||||||
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 Manually link channels
|
|
||||||
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
|
|
||||||
each channel in unlinkedChannels
|
|
||||||
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)
|
|
||||||
+discord(channel, true, "Announcement")
|
|
||||||
else
|
|
||||||
.s-empty-state.p8 All Discord channels are linked.
|
|
||||||
.fl-grow1.s-btn-group.fd-column.w30
|
|
||||||
each room in unlinkedRooms
|
|
||||||
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)
|
|
||||||
+matrix(room, true)
|
|
||||||
else
|
|
||||||
.s-empty-state.p8 All Matrix rooms are linked.
|
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
|
||||||
div
|
|
||||||
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
|
||||||
!= icons.Icons.IconMerge
|
|
||||||
= ` Link`
|
|
||||||
|
|
||||||
details.mt48
|
|
||||||
summary Debug room list
|
|
||||||
.d-grid.grid__2.gx24
|
|
||||||
div
|
div
|
||||||
h3.mt24 Channels
|
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
||||||
p Channels are read from the channel_room table and then merged with the discord.channels memory cache to make the merged list. Anything in memory cache that's not in channel_room is considered unlinked.
|
!= icons.Icons.IconMerge
|
||||||
div
|
= ` Link`
|
||||||
h3.mt24 Rooms
|
|
||||||
p Rooms use the same merged list as channels, based on augmented channel_room data. Then, rooms are read from the space. Anything in the space that's not merged is considered unlinked.
|
details.mt48
|
||||||
div
|
summary Debug room list
|
||||||
h3.mt24 Unavailable channels: Deleted from Discord
|
.d-grid.grid__2.gx24
|
||||||
.s-card.p0
|
div
|
||||||
ul.my8.ml24
|
h3.mt24 Channels
|
||||||
each row in removedUncachedChannels
|
p Channels are read from the channel_room table and then merged with the discord.channels memory cache to make the merged list. Anything in memory cache that's not in channel_room is considered unlinked.
|
||||||
li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`)= row.nick || row.name
|
div
|
||||||
h3.mt24 Unavailable channels: Wrong type
|
h3.mt24 Rooms
|
||||||
.s-card.p0
|
p Rooms use the same merged list as channels, based on augmented channel_room data. Then, rooms are read from the space. Anything in the space that's not merged is considered unlinked.
|
||||||
ul.my8.ml24
|
div
|
||||||
each row in removedWrongTypeChannels
|
h3.mt24 Unavailable channels: Deleted from Discord
|
||||||
li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`) (#{row.type}) #{row.name}
|
.s-card.p0
|
||||||
div- // Rooms
|
ul.my8.ml24
|
||||||
h3.mt24 Unavailable rooms: Already linked
|
each row in removedUncachedChannels
|
||||||
.s-card.p0
|
li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`)= row.nick || row.name
|
||||||
ul.my8.ml24
|
h3.mt24 Unavailable channels: Wrong type
|
||||||
each row in removedLinkedRooms
|
.s-card.p0
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
ul.my8.ml24
|
||||||
h3.mt24 Unavailable rooms: Wrong type
|
each row in removedWrongTypeChannels
|
||||||
.s-card.p0
|
li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`) (#{row.type}) #{row.name}
|
||||||
ul.my8.ml24
|
div- // Rooms
|
||||||
each row in removedWrongTypeRooms
|
h3.mt24 Unavailable rooms: Already linked
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`) (#{row.room_type}) #{row.name}
|
.s-card.p0
|
||||||
h3.mt24 Unavailable rooms: Archived thread
|
ul.my8.ml24
|
||||||
.s-card.p0
|
each row in removedLinkedRooms
|
||||||
ul.my8.ml24
|
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||||
each row in removedArchivedThreadRooms
|
h3.mt24 Unavailable rooms: Wrong type
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
.s-card.p0
|
||||||
|
ul.my8.ml24
|
||||||
|
each row in removedWrongTypeRooms
|
||||||
|
li: a(href=`https://matrix.to/#/${row.room_id}`) (#{row.room_type}) #{row.name}
|
||||||
|
h3.mt24 Unavailable rooms: Archived thread
|
||||||
|
.s-card.p0
|
||||||
|
ul.my8.ml24
|
||||||
|
each row in removedArchivedThreadRooms
|
||||||
|
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
extends includes/template.pug
|
extends includes/template.pug
|
||||||
|
|
||||||
block body
|
block body
|
||||||
if !managed
|
if !session.data.user_id
|
||||||
.s-empty-state.wmx4.p48
|
.s-empty-state.wmx4.p48
|
||||||
!= icons.Spots.SpotEmptyXL
|
!= icons.Spots.SpotEmptyXL
|
||||||
p You need to log in to manage your servers.
|
p You need to log in to manage your servers.
|
||||||
|
52
src/web/pug/guild_not_linked.pug
Normal file
52
src/web/pug/guild_not_linked.pug
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
extends includes/template.pug
|
||||||
|
|
||||||
|
mixin space(space)
|
||||||
|
.s-user-card.flex__1
|
||||||
|
span.s-avatar.s-avatar__32.s-user-card--avatar
|
||||||
|
if space.avatar
|
||||||
|
img.s-avatar--image(src=mUtils.getPublicUrlForMxc(space.avatar))
|
||||||
|
else
|
||||||
|
.s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= space.name[0]
|
||||||
|
.s-user-card--info.ai-start
|
||||||
|
strong= space.name
|
||||||
|
if space.topic
|
||||||
|
ul.s-user-card--awards
|
||||||
|
li space.topic
|
||||||
|
|
||||||
|
block body
|
||||||
|
.s-notice.s-notice__info.d-flex.g16
|
||||||
|
div
|
||||||
|
!= icons.Icons.IconInfo
|
||||||
|
div
|
||||||
|
strong You picked self-service mode
|
||||||
|
.mt4 To complete setup, you need to manually choose a Matrix space to link with #[strong= guild.name].
|
||||||
|
|
||||||
|
h3.mt32.fs-category Choose a space
|
||||||
|
|
||||||
|
form.s-card.bs-sm.p0.s-table-container.bar-md(method="post" action="/api/link-space")
|
||||||
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
|
table.s-table.s-table__bx-simple
|
||||||
|
each space in spaces
|
||||||
|
tr
|
||||||
|
td.p0: +space(space)
|
||||||
|
td: button.s-btn(name="space_id" value=space.room_id hx-post="/api/link-space" hx-trigger="click" hx-disabled-elt="this") Link with this space
|
||||||
|
else
|
||||||
|
if session.data.mxid
|
||||||
|
tr
|
||||||
|
- const self = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||||
|
td.p16 On Matrix, invite #[code.s-code-block: a.s-link(href=`https://matrix.to/#/${self}` target="_blank")= self] to a space. Then you can pick it from this list.
|
||||||
|
else
|
||||||
|
tr
|
||||||
|
td.d-flex.ai-center.pl16.g16
|
||||||
|
| You need to log in with Matrix first.
|
||||||
|
a.s-btn.s-btn__matrix.s-btn__outlined(href=rel("/log-in-with-matrix")) Log in with Matrix
|
||||||
|
|
||||||
|
h3.mt48.fs-category Auto-create
|
||||||
|
.s-card
|
||||||
|
form.d-flex.ai-center.g8(method="post" action="/api/autocreate" hx-post="/api/autocreate" hx-indicator="#easy-mode-button")
|
||||||
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
|
input(type="hidden" name="autocreate" value="true")
|
||||||
|
label.s-label.fl-grow1
|
||||||
|
| Changed your mind?
|
||||||
|
p.s-description If you want, OOYE can create and manage the Matrix space so you don't have to.
|
||||||
|
button.s-btn.s-btn__outlined#easy-mode-button Use easy mode
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
const assert = require("assert/strict")
|
const assert = require("assert/strict")
|
||||||
const {z} = require("zod")
|
const {z} = require("zod")
|
||||||
const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3")
|
const {defineEventHandler, useSession, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect} = require("h3")
|
||||||
|
|
||||||
const {as, db, sync} = require("../../passthrough")
|
const {as, db, sync, select} = require("../../passthrough")
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
/** @type {import("../../d2m/actions/create-space")} */
|
/** @type {import("../../d2m/actions/create-space")} */
|
||||||
@@ -29,6 +29,17 @@ as.router.post("/api/autocreate", defineEventHandler(async event => {
|
|||||||
if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||||
|
|
||||||
db.prepare("UPDATE guild_active SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id)
|
db.prepare("UPDATE guild_active SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id)
|
||||||
|
|
||||||
|
// If showing a partial page due to incomplete setup, need to refresh the whole page to show the alternate version
|
||||||
|
const spaceID = select("guild_space", "space_id", {guild_id: parsedBody.guild_id}).pluck().get()
|
||||||
|
if (!spaceID) {
|
||||||
|
if (getRequestHeader(event, "HX-Request")) {
|
||||||
|
setResponseHeader(event, "HX-Refresh", "true")
|
||||||
|
} else {
|
||||||
|
return sendRedirect(event, "", 302)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null // 204
|
return null // 204
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ const {LRUCache} = require("lru-cache")
|
|||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
const uqr = require("uqr")
|
const uqr = require("uqr")
|
||||||
|
|
||||||
const {discord, as, sync, select} = require("../../passthrough")
|
const {discord, as, sync, select, from, db} = require("../../passthrough")
|
||||||
/** @type {import("../pug-sync")} */
|
/** @type {import("../pug-sync")} */
|
||||||
const pugSync = sync.require("../pug-sync")
|
const pugSync = sync.require("../pug-sync")
|
||||||
/** @type {import("../../d2m/actions/create-space")} */
|
/** @type {import("../../d2m/actions/create-space")} */
|
||||||
@@ -109,15 +109,21 @@ function getChannelRoomsLinks(guildID, rooms) {
|
|||||||
as.router.get("/guild", defineEventHandler(async event => {
|
as.router.get("/guild", defineEventHandler(async event => {
|
||||||
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
|
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
|
||||||
const session = await useSession(event, {password: reg.as_token})
|
const session = await useSession(event, {password: reg.as_token})
|
||||||
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get()
|
const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const guild = discord.guilds.get(guild_id)
|
const guild = discord.guilds.get(guild_id)
|
||||||
|
|
||||||
// Permission problems
|
// Permission problems
|
||||||
if (!guild_id || !guild || !(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id)) {
|
if (!guild_id || !guild || !(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id) || !row) {
|
||||||
return pugSync.render(event, "guild_access_denied.pug", {guild_id})
|
return pugSync.render(event, "guild_access_denied.pug", {guild_id})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self-service guild that hasn't been linked yet - needs a special page encouraging the link flow
|
||||||
|
if (!row.space_id && row.autocreate === 0) {
|
||||||
|
const spaces = db.prepare("SELECT room_id, type, name, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id WHERE mxid = ? AND space_id IS NULL and type = 'm.space'").all(session.data.mxid)
|
||||||
|
return pugSync.render(event, "guild_not_linked.pug", {guild, guild_id, spaces})
|
||||||
|
}
|
||||||
|
|
||||||
const nonce = randomUUID()
|
const nonce = randomUUID()
|
||||||
validNonce.set(nonce, guild_id)
|
validNonce.set(nonce, guild_id)
|
||||||
|
|
||||||
@@ -128,10 +134,10 @@ as.router.get("/guild", defineEventHandler(async event => {
|
|||||||
const svg = generatedSvg.replace(/viewBox="0 0 ([0-9]+) ([0-9]+)"/, `data-nonce="${nonce}" width="$1" height="$2" $&`)
|
const svg = generatedSvg.replace(/viewBox="0 0 ([0-9]+) ([0-9]+)"/, `data-nonce="${nonce}" width="$1" height="$2" $&`)
|
||||||
assert.notEqual(svg, generatedSvg)
|
assert.notEqual(svg, generatedSvg)
|
||||||
|
|
||||||
// Unlinked guild
|
// Easy mode guild that hasn't been linked yet - need to remove elements that would require an existing space
|
||||||
if (!row) {
|
if (!row.space_id) {
|
||||||
const links = getChannelRoomsLinks(guild_id, [])
|
const links = getChannelRoomsLinks(guild_id, [])
|
||||||
return pugSync.render(event, "guild.pug", {guild, guild_id, svg, ...links})
|
return pugSync.render(event, "guild.pug", {guild, guild_id, svg, ...links, ...row})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linked guild
|
// Linked guild
|
||||||
|
@@ -18,6 +18,7 @@ test("web guild: access denied when not logged in", async t => {
|
|||||||
test("web guild: asks to select guild if not selected", async t => {
|
test("web guild: asks to select guild if not selected", async t => {
|
||||||
const content = await router.test("get", "/guild", {
|
const content = await router.test("get", "/guild", {
|
||||||
sessionData: {
|
sessionData: {
|
||||||
|
user_id: "1",
|
||||||
managedGuilds: []
|
managedGuilds: []
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -27,6 +28,7 @@ test("web guild: asks to select guild if not selected", async t => {
|
|||||||
test("web guild: access denied when guild id messed up", async t => {
|
test("web guild: access denied when guild id messed up", async t => {
|
||||||
const content = await router.test("get", "/guild?guild_id=1", {
|
const content = await router.test("get", "/guild?guild_id=1", {
|
||||||
sessionData: {
|
sessionData: {
|
||||||
|
user_id: "1",
|
||||||
managedGuilds: []
|
managedGuilds: []
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -43,6 +45,7 @@ test("web invite: access denied with invalid nonce", async t => {
|
|||||||
test("web guild: can view unbridged guild", async t => {
|
test("web guild: can view unbridged guild", async t => {
|
||||||
const content = await router.test("get", "/guild?guild_id=66192955777486848", {
|
const content = await router.test("get", "/guild?guild_id=66192955777486848", {
|
||||||
sessionData: {
|
sessionData: {
|
||||||
|
user_id: "1",
|
||||||
managedGuilds: ["66192955777486848"]
|
managedGuilds: ["66192955777486848"]
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
|
@@ -16,6 +16,10 @@ const {reg} = require("../../matrix/read-registration")
|
|||||||
const api = sync.require("../../matrix/api")
|
const api = sync.require("../../matrix/api")
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
|
linkSpace: z.object({
|
||||||
|
guild_id: z.string(),
|
||||||
|
space_id: z.string()
|
||||||
|
}),
|
||||||
link: z.object({
|
link: z.object({
|
||||||
guild_id: z.string(),
|
guild_id: z.string(),
|
||||||
matrix: z.string(),
|
matrix: z.string(),
|
||||||
@@ -27,6 +31,48 @@ const schema = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
|
const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
|
||||||
|
const session = await useSession(event, {password: reg.as_token})
|
||||||
|
|
||||||
|
// Check guild ID
|
||||||
|
const guildID = parsedBody.guild_id
|
||||||
|
if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
||||||
|
|
||||||
|
// Check space ID
|
||||||
|
if (!session.data.mxid) throw createError({status: 403, message: "Forbidden", data: "Can't link with your Matrix space if you aren't logged in to Matrix"})
|
||||||
|
const spaceID = parsedBody.space_id
|
||||||
|
const inviteType = select("invite", "type", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
|
||||||
|
if (inviteType !== "m.space") throw createError({status: 403, message: "Forbidden", data: "No past invitations detected from your Matrix account for that space."})
|
||||||
|
|
||||||
|
// Check they are not already bridged
|
||||||
|
const existing = select("guild_space", "guild_id", {}, "WHERE guild_id = ? OR space_id = ?").get(guildID, spaceID)
|
||||||
|
if (existing) throw createError({status: 400, message: "Bad Request", data: `Guild ID ${guildID} or space ID ${spaceID} are already bridged and cannot be reused`})
|
||||||
|
|
||||||
|
// Check space exists and bridge is joined and bridge has PL 100
|
||||||
|
const self = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||||
|
/** @type {Ty.Event.M_Room_Member} */
|
||||||
|
const memberEvent = await api.getStateEvent(spaceID, "m.room.member", self)
|
||||||
|
if (memberEvent.membership !== "join") throw createError({status: 400, message: "Bad Request", data: "Matrix space does not exist"})
|
||||||
|
/** @type {Ty.Event.M_Power_Levels} */
|
||||||
|
const powerLevelsStateContent = await api.getStateEvent(spaceID, "m.room.power_levels", "")
|
||||||
|
const selfPowerLevel = powerLevelsStateContent.users?.[self] || powerLevelsStateContent.users_default || 0
|
||||||
|
if (selfPowerLevel < (powerLevelsStateContent.state_default || 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix space"})
|
||||||
|
|
||||||
|
// Check inviting user is a moderator in the space
|
||||||
|
const invitingPowerLevel = powerLevelsStateContent.users?.[session.data.mxid] || powerLevelsStateContent.users_default || 0
|
||||||
|
if (invitingPowerLevel < (powerLevelsStateContent.state_default || 50)) throw createError({status: 403, message: "Forbidden", data: `You need to be at least power level 50 (moderator) in the target Matrix space to use OOYE, but you are currently power level ${invitingPowerLevel}.`})
|
||||||
|
|
||||||
|
// Insert database entry
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guildID, spaceID)
|
||||||
|
db.prepare("DELETE FROM invite WHERE room_id = ?").run(spaceID)
|
||||||
|
})()
|
||||||
|
|
||||||
|
setResponseHeader(event, "HX-Refresh", "true")
|
||||||
|
return null // 204
|
||||||
|
}))
|
||||||
|
|
||||||
as.router.post("/api/link", defineEventHandler(async event => {
|
as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.link.parse)
|
const parsedBody = await readValidatedBody(event, schema.link.parse)
|
||||||
const session = await useSession(event, {password: reg.as_token})
|
const session = await useSession(event, {password: reg.as_token})
|
||||||
|
@@ -7,15 +7,18 @@ const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHead
|
|||||||
const icons = require("@stackoverflow/stacks-icons")
|
const icons = require("@stackoverflow/stacks-icons")
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
const dUtils = require("../discord/utils")
|
const dUtils = require("../discord/utils")
|
||||||
|
const reg = require("../matrix/read-registration")
|
||||||
|
|
||||||
const {sync, discord, as, select} = require("../passthrough")
|
const {sync, discord, as, select} = require("../passthrough")
|
||||||
/** @type {import("./pug-sync")} */
|
/** @type {import("./pug-sync")} */
|
||||||
const pugSync = sync.require("./pug-sync")
|
const pugSync = sync.require("./pug-sync")
|
||||||
|
/** @type {import("../m2d/converters/utils")} */
|
||||||
|
const mUtils = sync.require("../m2d/converters/utils")
|
||||||
const {id} = require("../../addbot")
|
const {id} = require("../../addbot")
|
||||||
|
|
||||||
// Pug
|
// Pug
|
||||||
|
|
||||||
pugSync.addGlobals({id, h3, discord, select, DiscordTypes, dUtils, icons})
|
pugSync.addGlobals({id, h3, discord, select, DiscordTypes, dUtils, mUtils, icons, reg: reg.reg})
|
||||||
pugSync.createRoute(as.router, "/", "home.pug")
|
pugSync.createRoute(as.router, "/", "home.pug")
|
||||||
pugSync.createRoute(as.router, "/ok", "ok.pug")
|
pugSync.createRoute(as.router, "/ok", "ok.pug")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user