Compare commits
6 commits
main
...
feat/add-u
Author | SHA1 | Date | |
---|---|---|---|
16a9c643b8 | |||
bbe416a42e | |||
4c4e17e520 | |||
978de797ba | |||
bdb4867bb6 | |||
738459e617 |
8 changed files with 191 additions and 41 deletions
6
.editorconfig
Normal file
6
.editorconfig
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.pug]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
|
@ -146,7 +146,7 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) {
|
||||||
try {
|
try {
|
||||||
// API lookup
|
// API lookup
|
||||||
var pkMessage = await fetchMessage(messageID)
|
var pkMessage = await fetchMessage(messageID)
|
||||||
db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
|
db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fall back to offline cache
|
// Fall back to offline cache
|
||||||
const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get()
|
const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get()
|
||||||
|
|
|
@ -33,10 +33,9 @@ function getDiscordParseCallbacks(message, guild, useHTML) {
|
||||||
user: node => {
|
user: node => {
|
||||||
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
|
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
|
||||||
const interaction = message.interaction_metadata || message.interaction
|
const interaction = message.interaction_metadata || message.interaction
|
||||||
const username = message.mentions?.find(ment => ment.id === node.id)?.username
|
const username = message.mentions.find(ment => ment.id === node.id)?.username
|
||||||
|| message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username
|
|| message.referenced_message?.mentions.find(ment => ment.id === node.id)?.username
|
||||||
|| (interaction?.user.id === node.id ? interaction.user.username : null)
|
|| (interaction?.user.id === node.id ? interaction.user.username : null)
|
||||||
|| (message.author.id === node.id ? message.author.username : null)
|
|
||||||
|| node.id
|
|| node.id
|
||||||
if (mxid && useHTML) {
|
if (mxid && useHTML) {
|
||||||
return `<a href="https://matrix.to/#/${mxid}">@${username}</a>`
|
return `<a href="https://matrix.to/#/${mxid}">@${username}</a>`
|
||||||
|
|
|
@ -24,3 +24,7 @@ test("api path: real world mxid", t => {
|
||||||
test("api path: extras number works", t => {
|
test("api path: extras number works", t => {
|
||||||
t.equal(path(`/client/v3/rooms/!example/timestamp_to_event`, null, {ts: 1687324651120}), "/client/v3/rooms/!example/timestamp_to_event?ts=1687324651120")
|
t.equal(path(`/client/v3/rooms/!example/timestamp_to_event`, null, {ts: 1687324651120}), "/client/v3/rooms/!example/timestamp_to_event?ts=1687324651120")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("api path: multiple via params", t => {
|
||||||
|
t.equal(path(`/client/v3/rooms/!example/join`, null, {via: ["cadence.moe", "matrix.org"], ts: 1687324651120}), "/client/v3/rooms/!example/join?via=cadence.moe&via=matrix.org&ts=1687324651120")
|
||||||
|
})
|
||||||
|
|
|
@ -54,6 +54,10 @@ block body
|
||||||
.s-page-title.mb24
|
.s-page-title.mb24
|
||||||
h1.s-page-title--header= guild.name
|
h1.s-page-title--header= guild.name
|
||||||
|
|
||||||
|
form(method="post" action=rel("/api/unlink-space") hx-confirm="Do you want to unlink this server?\nThis will unlink every channels listed below.\nIt may take a moment to clean up Matrix resources.")
|
||||||
|
input(type="hidden" name="guild_id" value=guild.id)
|
||||||
|
button.s-btn.s-btn__muted.s-btn__xs(hx-post=rel("/api/unlink-space") hx-trigger="click" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
|
||||||
|
|
||||||
.d-flex.g16(class="sm:fw-wrap")
|
.d-flex.g16(class="sm:fw-wrap")
|
||||||
.fl-grow1
|
.fl-grow1
|
||||||
h2.fs-headline1 Invite a Matrix user
|
h2.fs-headline1 Invite a Matrix user
|
||||||
|
|
|
@ -12,6 +12,8 @@ const auth = sync.require("../auth")
|
||||||
const mreq = sync.require("../../matrix/mreq")
|
const mreq = sync.require("../../matrix/mreq")
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
|
|
||||||
|
const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {H3Event} event
|
* @param {H3Event} event
|
||||||
* @returns {import("../../matrix/api")}
|
* @returns {import("../../matrix/api")}
|
||||||
|
@ -39,6 +41,60 @@ function getCreateSpace(event) {
|
||||||
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
|
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {H3Event} event
|
||||||
|
* @param {string} guild_id
|
||||||
|
*/
|
||||||
|
async function validateUserHaveRightsOnGuild(event, guild_id) {
|
||||||
|
const managed = await auth.getManagedGuilds(event)
|
||||||
|
if (!managed.has(guild_id))
|
||||||
|
throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {H3Event} event
|
||||||
|
* @param {string} guild_id
|
||||||
|
* @returns {Promise<DiscordTypes.APIGuild & {members: DiscordTypes.APIGuildMember[]}>}
|
||||||
|
*/
|
||||||
|
async function validateGuildAccess(event, guild_id) {
|
||||||
|
// Check guild ID or nonce
|
||||||
|
await validateUserHaveRightsOnGuild(event, guild_id)
|
||||||
|
|
||||||
|
// Check guild exists
|
||||||
|
const guild = discord.guilds.get(guild_id)
|
||||||
|
if (!guild)
|
||||||
|
throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"})
|
||||||
|
|
||||||
|
return guild
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {H3Event} event
|
||||||
|
* @param {string} channel_id
|
||||||
|
* @param {string} guild_id
|
||||||
|
*/
|
||||||
|
async function doRoomUnlink(event, channel_id, guild_id) {
|
||||||
|
const createRoom = getCreateRoom(event)
|
||||||
|
|
||||||
|
// Check that the channel (if it exists) is part of this guild
|
||||||
|
/** @type {any} */
|
||||||
|
let channel = discord.channels.get(channel_id)
|
||||||
|
if (channel) {
|
||||||
|
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}`})
|
||||||
|
} else {
|
||||||
|
// Otherwise, if the channel isn't cached, it must have been deleted.
|
||||||
|
// There's no other authentication here - it's okay for anyone to unlink a deleted channel just by knowing its ID.
|
||||||
|
channel = {id: channel_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)
|
||||||
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
linkSpace: z.object({
|
linkSpace: z.object({
|
||||||
guild_id: z.string(),
|
guild_id: z.string(),
|
||||||
|
@ -52,18 +108,20 @@ const schema = {
|
||||||
unlink: z.object({
|
unlink: z.object({
|
||||||
guild_id: z.string(),
|
guild_id: z.string(),
|
||||||
channel_id: z.string()
|
channel_id: z.string()
|
||||||
})
|
}),
|
||||||
|
unlinkSpace: z.object({
|
||||||
|
guild_id: z.string(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
as.router.post("/api/link-space", defineEventHandler(async event => {
|
as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
|
const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
|
||||||
const session = await auth.useSession(event)
|
const session = await auth.useSession(event)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
|
|
||||||
// Check guild ID
|
// Check guild ID
|
||||||
const guildID = parsedBody.guild_id
|
const guildID = parsedBody.guild_id
|
||||||
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
await validateUserHaveRightsOnGuild(event, guildID)
|
||||||
|
|
||||||
// Check space ID
|
// 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"})
|
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"})
|
||||||
|
@ -83,7 +141,6 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bridge has PL 100
|
// Check bridge has PL 100
|
||||||
const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
|
||||||
/** @type {Ty.Event.M_Power_Levels?} */
|
/** @type {Ty.Event.M_Power_Levels?} */
|
||||||
let powerLevelsStateContent = null
|
let powerLevelsStateContent = null
|
||||||
try {
|
try {
|
||||||
|
@ -108,18 +165,12 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
|
|
||||||
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 managed = await auth.getManagedGuilds(event)
|
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
const createRoom = getCreateRoom(event)
|
const createRoom = getCreateRoom(event)
|
||||||
const createSpace = getCreateSpace(event)
|
const createSpace = getCreateSpace(event)
|
||||||
|
|
||||||
// Check guild ID or nonce
|
|
||||||
const guildID = parsedBody.guild_id
|
const guildID = parsedBody.guild_id
|
||||||
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
const guild = await validateGuildAccess(event, guildID)
|
||||||
|
|
||||||
// Check guild is bridged
|
|
||||||
const guild = discord.guilds.get(guildID)
|
|
||||||
if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"})
|
|
||||||
const spaceID = await createSpace.ensureSpace(guild)
|
const spaceID = await createSpace.ensureSpace(guild)
|
||||||
|
|
||||||
// Check channel exists
|
// Check channel exists
|
||||||
|
@ -183,33 +234,44 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
|
|
||||||
as.router.post("/api/unlink", defineEventHandler(async event => {
|
as.router.post("/api/unlink", defineEventHandler(async event => {
|
||||||
const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
|
const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
await validateGuildAccess(event, guild_id)
|
||||||
const createRoom = getCreateRoom(event)
|
|
||||||
|
|
||||||
// Check guild ID or nonce
|
await doRoomUnlink(event, channel_id, guild_id)
|
||||||
if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
|
|
||||||
|
setResponseHeader(event, "HX-Refresh", "true")
|
||||||
// Check guild exists
|
return null // 204
|
||||||
const guild = discord.guilds.get(guild_id)
|
}))
|
||||||
if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"})
|
|
||||||
|
as.router.post("/api/unlink-space", defineEventHandler(async event => {
|
||||||
// Check that the channel (if it exists) is part of this guild
|
const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse)
|
||||||
/** @type {any} */
|
const api = getAPI(event)
|
||||||
let channel = discord.channels.get(channel_id)
|
await validateGuildAccess(event, guild_id)
|
||||||
if (channel) {
|
|
||||||
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}`})
|
const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get()
|
||||||
} else {
|
if (!spaceID)
|
||||||
// Otherwise, if the channel isn't cached, it must have been deleted.
|
throw createError({status: 400, message: "Bad Request", data: "Matrix space does not exist or bot has not linked it"})
|
||||||
// There's no other authentication here - it's okay for anyone to unlink a deleted channel just by knowing its ID.
|
|
||||||
channel = {id: channel_id}
|
const linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
|
||||||
}
|
|
||||||
|
for (const channel of linkedChannels) {
|
||||||
// Check channel is currently bridged
|
await doRoomUnlink(event, channel.channel_id, guild_id)
|
||||||
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`})
|
|
||||||
|
const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
|
||||||
// Do it
|
if (remainingLinkedChannels.length !== 0)
|
||||||
await createRoom.unbridgeDeletedChannel(channel, guild_id)
|
throw createError({status: 400, message: "Bad Request", data: "Some linked room still exists after trying to unlink all of them. Aborting the space unlinking..."})
|
||||||
|
|
||||||
|
await api.setUserPower(spaceID, me, 0)
|
||||||
|
await api.leaveRoom(spaceID)
|
||||||
|
|
||||||
|
db.prepare("DELETE FROM guild_space WHERE guild_id=? AND space_id=?").run(guild_id, spaceID)
|
||||||
|
|
||||||
|
// NOTE: not deleting from guild_active as this can lead to inconsistent state:
|
||||||
|
// if we only delete from DB, the guild is still displayed on the top-right dropdown,
|
||||||
|
// but when selected we get the "Please add the bot to your server using the buttons on the home page." page
|
||||||
|
//
|
||||||
|
// So either keep as-is, or delete from guild_active, but also leave the discord guild? Not sure if we want that or not
|
||||||
|
// db.prepare("DELETE FROM guild_active WHERE guild_id=?").run(guild_id)
|
||||||
|
|
||||||
setResponseHeader(event, "HX-Refresh", "true")
|
setResponseHeader(event, "HX-Refresh", "true")
|
||||||
return null // 204
|
return null // 204
|
||||||
|
|
|
@ -630,3 +630,76 @@ test("web unlink room: checks that the channel is bridged", async t => {
|
||||||
}))
|
}))
|
||||||
t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
|
t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// *****
|
||||||
|
|
||||||
|
test("web unlink space: access denied if not logged in to Discord", async t => {
|
||||||
|
const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
|
||||||
|
body: {
|
||||||
|
guild_id: "665289423482519565"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.equal(error.data, "Can't edit a guild you don't have Manage Server permissions in")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("web unlink space: checks that guild exists", async t => {
|
||||||
|
const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
|
||||||
|
sessionData: {
|
||||||
|
managedGuilds: ["2"]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
guild_id: "2"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.equal(error.data, "Discord guild does not exist or bot has not joined it")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("web unlink space: checks that a space is linked to the guild", async t => {
|
||||||
|
const row = db.prepare("SELECT * FROM guild_space WHERE guild_id = '665289423482519565'").get()
|
||||||
|
db.prepare("DELETE FROM guild_space WHERE guild_id = '665289423482519565'").run()
|
||||||
|
|
||||||
|
const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
|
||||||
|
sessionData: {
|
||||||
|
managedGuilds: ["665289423482519565"]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
guild_id: "665289423482519565"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.equal(error.data, "Matrix space does not exist or bot has not linked it")
|
||||||
|
|
||||||
|
db.prepare("INSERT INTO guild_space (guild_id, space_id, privacy_level, presence, url_preview) VALUES (?, ?, ?, ?, ?)").run(row.guild_id, row.space_id, row.privacy_level, row.presence, row.url_preview)
|
||||||
|
const new_row = db.prepare("SELECT * FROM guild_space WHERE guild_id = '665289423482519565'").get()
|
||||||
|
t.deepEqual(row, new_row)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("web unlink space: successfully calls unbridgeDeletedChannel on linked channels in space", async t => {
|
||||||
|
// Need to re-link the room to check it is properly unlinked by the unlink-space
|
||||||
|
await router.test("post", "/api/link", {
|
||||||
|
sessionData: {
|
||||||
|
managedGuilds: ["665289423482519565"]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
discord: "665310973967597573",
|
||||||
|
matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||||
|
guild_id: "665289423482519565"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let called = 0
|
||||||
|
await router.test("post", "/api/unlink-space", {
|
||||||
|
sessionData: {
|
||||||
|
managedGuilds: ["665289423482519565"]
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
guild_id: "665289423482519565",
|
||||||
|
},
|
||||||
|
createRoom: {
|
||||||
|
async unbridgeDeletedChannel(channel) {
|
||||||
|
called++
|
||||||
|
t.equal(channel.id, "665310973967597573")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(called, 1)
|
||||||
|
})
|
||||||
|
|
|
@ -17,6 +17,8 @@ const {reg} = require("../src/matrix/read-registration")
|
||||||
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
|
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
|
||||||
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
||||||
reg.ooye.server_name = "cadence.moe"
|
reg.ooye.server_name = "cadence.moe"
|
||||||
|
reg.ooye.namespace_prefix = "_ooye_"
|
||||||
|
reg.sender_localpart = "_ooye_bot"
|
||||||
reg.id = "baby"
|
reg.id = "baby"
|
||||||
reg.as_token = "don't actually take authenticated actions on the server"
|
reg.as_token = "don't actually take authenticated actions on the server"
|
||||||
reg.hs_token = "don't actually take authenticated actions on the server"
|
reg.hs_token = "don't actually take authenticated actions on the server"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue