Compare commits
	
		
			6 commits
		
	
	
		
			10d14bbdaa
			...
			16309f26b3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 16309f26b3 | |||
| 5aff6f9048 | |||
| dfc61594f6 | |||
| 3597a3b5ce | |||
| 05d788e263 | |||
| d95a114377 | 
					 9 changed files with 365 additions and 55 deletions
				
			
		
							
								
								
									
										9
									
								
								jsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								jsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
{
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"target": "es2024",
 | 
			
		||||
		"module": "nodenext",
 | 
			
		||||
		"strict": true,
 | 
			
		||||
		"noImplicitAny": false,
 | 
			
		||||
		"useUnknownInCatchVariables": false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -207,6 +207,7 @@ async function attachmentToEvent(mentions, attachment) {
 | 
			
		|||
 * - alwaysReturnFormattedBody: false - formatted_body will be skipped if it is the same as body because the message is plaintext. if you want the formatted_body to be returned anyway, for example to merge it with another message, then set this to true.
 | 
			
		||||
 * - scanTextForMentions: true - needs to be set to false when converting forwarded messages etc which may be from a different channel that can't be scanned.
 | 
			
		||||
 * @param {{api: import("../../matrix/api"), snow?: import("snowtransfer").SnowTransfer}} di simple-as-nails dependency injection for the matrix API
 | 
			
		||||
 * @returns {Promise<{$type: string, $sender?: string, [x: string]: any}[]>}
 | 
			
		||||
 */
 | 
			
		||||
async function messageToEvent(message, guild, options = {}, di) {
 | 
			
		||||
	const events = []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,11 @@ function path(p, mxid, otherParams = {}) {
 | 
			
		|||
	const u = new URL(p, "http://localhost")
 | 
			
		||||
	if (mxid) u.searchParams.set("user_id", mxid)
 | 
			
		||||
	for (const entry of Object.entries(otherParams)) {
 | 
			
		||||
		if (entry[1] != undefined) {
 | 
			
		||||
		if (Array.isArray(entry[1])) {
 | 
			
		||||
			for (const element of entry[1]) {
 | 
			
		||||
				u.searchParams.append(entry[0], element)
 | 
			
		||||
			}
 | 
			
		||||
		} else if (entry[1] != undefined) {
 | 
			
		||||
			u.searchParams.set(entry[0], entry[1])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,11 +66,14 @@ async function createRoom(content) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {string} roomIDOrAlias
 | 
			
		||||
 * @param {string?} [mxid]
 | 
			
		||||
 * @param {string[]?} [via]
 | 
			
		||||
 * @returns {Promise<string>} room ID
 | 
			
		||||
 */
 | 
			
		||||
async function joinRoom(roomIDOrAlias, mxid) {
 | 
			
		||||
async function joinRoom(roomIDOrAlias, mxid, via) {
 | 
			
		||||
	/** @type {Ty.R.RoomJoined} */
 | 
			
		||||
	const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid), {})
 | 
			
		||||
	const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid, {via}), {})
 | 
			
		||||
	return root.room_id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,3 +24,7 @@ test("api path: real world mxid", 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")
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								src/types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								src/types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -149,6 +149,14 @@ export namespace Event {
 | 
			
		|||
		prev_content?: any
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export type StrippedChildStateEvent = {
 | 
			
		||||
		type: string
 | 
			
		||||
		state_key: string
 | 
			
		||||
		sender: string
 | 
			
		||||
		origin_server_ts: number
 | 
			
		||||
		content: any
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export type M_Room_Message = {
 | 
			
		||||
		msgtype: "m.text" | "m.emote"
 | 
			
		||||
		body: string
 | 
			
		||||
| 
						 | 
				
			
			@ -345,7 +353,7 @@ export namespace R {
 | 
			
		|||
	export type Hierarchy = {
 | 
			
		||||
		avatar_url?: string
 | 
			
		||||
		canonical_alias?: string
 | 
			
		||||
		children_state: {}
 | 
			
		||||
		children_state: Event.StrippedChildStateEvent[]
 | 
			
		||||
		guest_can_join: boolean
 | 
			
		||||
		join_rule?: string
 | 
			
		||||
		name?: string
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,10 @@ block body
 | 
			
		|||
  .s-page-title.mb24
 | 
			
		||||
    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")
 | 
			
		||||
    .fl-grow1
 | 
			
		||||
      h2.fs-headline1 Invite a Matrix user
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,8 @@ const auth = sync.require("../auth")
 | 
			
		|||
const mreq = sync.require("../../matrix/mreq")
 | 
			
		||||
const {reg} = require("../../matrix/read-registration")
 | 
			
		||||
 | 
			
		||||
const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {H3Event} event
 | 
			
		||||
 * @returns {import("../../matrix/api")}
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +41,60 @@ function getCreateSpace(event) {
 | 
			
		|||
	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 = {
 | 
			
		||||
	linkSpace: z.object({
 | 
			
		||||
		guild_id: z.string(),
 | 
			
		||||
| 
						 | 
				
			
			@ -52,18 +108,20 @@ const schema = {
 | 
			
		|||
	unlink: z.object({
 | 
			
		||||
		guild_id: z.string(),
 | 
			
		||||
		channel_id: z.string()
 | 
			
		||||
	})
 | 
			
		||||
	}),
 | 
			
		||||
	unlinkSpace: z.object({
 | 
			
		||||
		guild_id: z.string(),
 | 
			
		||||
	}),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
as.router.post("/api/link-space", defineEventHandler(async event => {
 | 
			
		||||
	const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
 | 
			
		||||
	const session = await auth.useSession(event)
 | 
			
		||||
	const managed = await auth.getManagedGuilds(event)
 | 
			
		||||
	const api = getAPI(event)
 | 
			
		||||
 | 
			
		||||
	// Check 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
 | 
			
		||||
	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"})
 | 
			
		||||
| 
						 | 
				
			
			@ -75,15 +133,18 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
 | 
			
		|||
	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`})
 | 
			
		||||
 | 
			
		||||
	const inviteSender = select("invite", "mxid", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
 | 
			
		||||
	const inviteSenderServer = inviteSender?.match(/:(.*)/)?.[1]
 | 
			
		||||
	const via = [inviteSenderServer || ""]
 | 
			
		||||
 | 
			
		||||
	// Check space exists and bridge is joined
 | 
			
		||||
	try {
 | 
			
		||||
		await api.joinRoom(parsedBody.space_id)
 | 
			
		||||
		await api.joinRoom(parsedBody.space_id, null, via)
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
 | 
			
		||||
		throw createError({status: 400, message: "Unable To Join", data: `Unable to join the requested Matrix space. Please invite the bridge to the space and try again. (Server said: ${e.errcode} - ${e.message})`})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check bridge has PL 100
 | 
			
		||||
	const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
 | 
			
		||||
	/** @type {Ty.Event.M_Power_Levels?} */
 | 
			
		||||
	let powerLevelsStateContent = null
 | 
			
		||||
	try {
 | 
			
		||||
| 
						 | 
				
			
			@ -108,18 +169,12 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
 | 
			
		|||
 | 
			
		||||
as.router.post("/api/link", defineEventHandler(async event => {
 | 
			
		||||
	const parsedBody = await readValidatedBody(event, schema.link.parse)
 | 
			
		||||
	const managed = await auth.getManagedGuilds(event)
 | 
			
		||||
	const api = getAPI(event)
 | 
			
		||||
	const createRoom = getCreateRoom(event)
 | 
			
		||||
	const createSpace = getCreateSpace(event)
 | 
			
		||||
 | 
			
		||||
	// Check guild ID or nonce
 | 
			
		||||
	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"})
 | 
			
		||||
 | 
			
		||||
	// 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 guild = await validateGuildAccess(event, guildID)
 | 
			
		||||
	const spaceID = await createSpace.ensureSpace(guild)
 | 
			
		||||
 | 
			
		||||
	// Check channel exists
 | 
			
		||||
| 
						 | 
				
			
			@ -134,19 +189,33 @@ as.router.post("/api/link", defineEventHandler(async event => {
 | 
			
		|||
	if (row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${row.channel_id} or room ID ${parsedBody.matrix} are already bridged and cannot be reused`})
 | 
			
		||||
 | 
			
		||||
	// Check room is part of the guild's space
 | 
			
		||||
	let found = false
 | 
			
		||||
	let foundRoom = false
 | 
			
		||||
	/** @type {string[]?} */
 | 
			
		||||
	let foundVia = null
 | 
			
		||||
	for await (const room of api.generateFullHierarchy(spaceID)) {
 | 
			
		||||
		// When finding a space during iteration, look at space's children state, because we need a `via` to join the room (when we find it later)
 | 
			
		||||
		for (const state of room.children_state) {
 | 
			
		||||
			if (state.type === "m.space.child" && state.state_key === parsedBody.matrix) {
 | 
			
		||||
				foundVia = state.content.via
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// When finding a room during iteration, see if it was the requested room (to confirm that the room is in the space)
 | 
			
		||||
		if (room.room_id === parsedBody.matrix && !room.room_type) {
 | 
			
		||||
			found = true
 | 
			
		||||
			break
 | 
			
		||||
			foundRoom = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (foundRoom && foundVia) break
 | 
			
		||||
	}
 | 
			
		||||
	if (!found) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"})
 | 
			
		||||
	if (!foundRoom) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"})
 | 
			
		||||
 | 
			
		||||
	// Check room exists and bridge is joined
 | 
			
		||||
	try {
 | 
			
		||||
		await api.joinRoom(parsedBody.matrix)
 | 
			
		||||
		await api.joinRoom(parsedBody.matrix, null, foundVia)
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		if (!foundVia) {
 | 
			
		||||
			throw createError({status: 400, message: "Unable To Join", data: `Unable to join the requested Matrix room. Please invite the bridge to the room and try again. (Server said: ${e.errcode} - ${e.message})`})
 | 
			
		||||
		}
 | 
			
		||||
		throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -183,33 +252,44 @@ as.router.post("/api/link", defineEventHandler(async event => {
 | 
			
		|||
 | 
			
		||||
as.router.post("/api/unlink", defineEventHandler(async event => {
 | 
			
		||||
	const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
 | 
			
		||||
	const managed = await auth.getManagedGuilds(event)
 | 
			
		||||
	const createRoom = getCreateRoom(event)
 | 
			
		||||
	await validateGuildAccess(event, guild_id)
 | 
			
		||||
 | 
			
		||||
	// Check guild ID or nonce
 | 
			
		||||
	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"})
 | 
			
		||||
 | 
			
		||||
	// 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"})
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
	await doRoomUnlink(event, channel_id, guild_id)
 | 
			
		||||
 | 
			
		||||
	setResponseHeader(event, "HX-Refresh", "true")
 | 
			
		||||
	return null // 204
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
as.router.post("/api/unlink-space", defineEventHandler(async event => {
 | 
			
		||||
	const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse)
 | 
			
		||||
	const api = getAPI(event)
 | 
			
		||||
	await validateGuildAccess(event, guild_id)
 | 
			
		||||
 | 
			
		||||
	const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get()
 | 
			
		||||
	if (!spaceID)
 | 
			
		||||
		throw createError({status: 400, message: "Bad Request", data: "Matrix space does not exist or bot has not linked it"})
 | 
			
		||||
 | 
			
		||||
	const linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
 | 
			
		||||
 | 
			
		||||
	for (const channel of linkedChannels) {
 | 
			
		||||
		await doRoomUnlink(event, channel.channel_id, guild_id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
 | 
			
		||||
	if (remainingLinkedChannels.length !== 0)
 | 
			
		||||
		throw createError({status: 500, message: "Internal Server Error", 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")
 | 
			
		||||
	return null // 204
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,7 @@ test("web link space: check that OOYE is joined", async t => {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}))
 | 
			
		||||
	t.equal(error.data, "M_FORBIDDEN - not allowed to join I guess")
 | 
			
		||||
	t.equal(error.data, "Unable to join the requested Matrix space. Please invite the bridge to the space and try again. (Server said: M_FORBIDDEN - not allowed to join I guess)")
 | 
			
		||||
	t.equal(called, 1)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -360,7 +360,7 @@ test("web link room: check that room is part of space (not in hierarchy)", async
 | 
			
		|||
	t.equal(called, 1)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test("web link room: check that bridge can join room", async t => {
 | 
			
		||||
test("web link room: check that bridge can join room (notices lack of via and asks for invite instead)", async t => {
 | 
			
		||||
	let called = 0
 | 
			
		||||
	const [error] = await tryToCatch(() => router.test("post", "/api/link", {
 | 
			
		||||
		sessionData: {
 | 
			
		||||
| 
						 | 
				
			
			@ -381,7 +381,55 @@ test("web link room: check that bridge can join room", async t => {
 | 
			
		|||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: {},
 | 
			
		||||
					children_state: [],
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
				/* c8 ignore next */
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}))
 | 
			
		||||
	t.equal(error.data, "Unable to join the requested Matrix room. Please invite the bridge to the room and try again. (Server said: M_FORBIDDEN - not allowed to join I guess)")
 | 
			
		||||
	t.equal(called, 2)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test("web link room: check that bridge can join room (uses via for join attempt)", async t => {
 | 
			
		||||
	let called = 0
 | 
			
		||||
	const [error] = await tryToCatch(() => router.test("post", "/api/link", {
 | 
			
		||||
		sessionData: {
 | 
			
		||||
			managedGuilds: ["665289423482519565"]
 | 
			
		||||
		},
 | 
			
		||||
		body: {
 | 
			
		||||
			discord: "665310973967597573",
 | 
			
		||||
			matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
			guild_id: "665289423482519565"
 | 
			
		||||
		},
 | 
			
		||||
		api: {
 | 
			
		||||
			async joinRoom(roomID, _, via) {
 | 
			
		||||
				called++
 | 
			
		||||
				t.deepEqual(via, ["cadence.moe", "hashi.re"])
 | 
			
		||||
				throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
 | 
			
		||||
			},
 | 
			
		||||
			async *generateFullHierarchy(spaceID) {
 | 
			
		||||
				called++
 | 
			
		||||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: [],
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
 | 
			
		||||
					children_state: [{
 | 
			
		||||
						type: "m.space.child",
 | 
			
		||||
						state_key: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
						sender: "@elliu:hashi.re",
 | 
			
		||||
						content: {
 | 
			
		||||
							via: ["cadence.moe", "hashi.re"]
 | 
			
		||||
						},
 | 
			
		||||
						origin_server_ts: 0
 | 
			
		||||
					}],
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -414,7 +462,7 @@ test("web link room: check that bridge has PL 100 in target room (event missing)
 | 
			
		|||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: {},
 | 
			
		||||
					children_state: [],
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -454,7 +502,7 @@ test("web link room: check that bridge has PL 100 in target room (users default)
 | 
			
		|||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: {},
 | 
			
		||||
					children_state: [],
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -494,7 +542,7 @@ test("web link room: successfully calls createRoom", async t => {
 | 
			
		|||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: {},
 | 
			
		||||
					children_state: [],
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -618,7 +666,9 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe
 | 
			
		|||
})
 | 
			
		||||
 | 
			
		||||
test("web unlink room: checks that the channel is bridged", async t => {
 | 
			
		||||
	const row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
 | 
			
		||||
	db.prepare("DELETE FROM channel_room WHERE channel_id = '665310973967597573'").run()
 | 
			
		||||
 | 
			
		||||
	const [error] = await tryToCatch(() => router.test("post", "/api/unlink", {
 | 
			
		||||
		sessionData: {
 | 
			
		||||
			managedGuilds: ["665289423482519565"]
 | 
			
		||||
| 
						 | 
				
			
			@ -629,4 +679,149 @@ test("web unlink room: checks that the channel is bridged", async t => {
 | 
			
		|||
		}
 | 
			
		||||
	}))
 | 
			
		||||
	t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
 | 
			
		||||
 | 
			
		||||
	db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_id, row.speedbump_checked, row.speedbump_webhook_id, row.guild_id, row.custom_topic)
 | 
			
		||||
	const new_row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
 | 
			
		||||
	t.deepEqual(row, new_row)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// *****
 | 
			
		||||
 | 
			
		||||
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 before trying to unlink the space", 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: correctly abort unlinking if some linked channels remain after trying to unlink them all", async t => {
 | 
			
		||||
	let unbridgedChannel = false
 | 
			
		||||
 | 
			
		||||
	const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
 | 
			
		||||
		sessionData: {
 | 
			
		||||
			managedGuilds: ["665289423482519565"]
 | 
			
		||||
		},
 | 
			
		||||
		body: {
 | 
			
		||||
			guild_id: "665289423482519565",
 | 
			
		||||
		},
 | 
			
		||||
		createRoom: {
 | 
			
		||||
			async unbridgeDeletedChannel(channel, guildID) {
 | 
			
		||||
				unbridgedChannel = true
 | 
			
		||||
				t.equal(channel.id, "665310973967597573")
 | 
			
		||||
				t.equal(guildID, "665289423482519565")
 | 
			
		||||
				// Do not actually delete the link from DB, should trigger error later in check
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		api: {
 | 
			
		||||
			async *generateFullHierarchy(spaceID) {
 | 
			
		||||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: {},
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
				/* c8 ignore next */
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	t.equal(error.data, "Some linked room still exists after trying to unlink all of them. Aborting the space unlinking...")
 | 
			
		||||
	t.equal(unbridgedChannel, true)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test("web unlink space: successfully calls unbridgeDeletedChannel on linked channels in space, self-downgrade power level, leave space, and delete link from DB", async t => {
 | 
			
		||||
	const {reg} = require("../../matrix/read-registration")
 | 
			
		||||
	const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
 | 
			
		||||
 | 
			
		||||
	const getLinkRowQuery = "SELECT * FROM guild_space WHERE guild_id = '665289423482519565'"
 | 
			
		||||
 | 
			
		||||
	const row = db.prepare(getLinkRowQuery).get()
 | 
			
		||||
	t.equal(row.space_id, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
 | 
			
		||||
	let unbridgedChannel = false
 | 
			
		||||
	let downgradedPowerLevel = false
 | 
			
		||||
	let leftRoom = false
 | 
			
		||||
	await router.test("post", "/api/unlink-space", {
 | 
			
		||||
		sessionData: {
 | 
			
		||||
			managedGuilds: ["665289423482519565"]
 | 
			
		||||
		},
 | 
			
		||||
		body: {
 | 
			
		||||
			guild_id: "665289423482519565",
 | 
			
		||||
		},
 | 
			
		||||
		createRoom: {
 | 
			
		||||
			async unbridgeDeletedChannel(channel, guildID) {
 | 
			
		||||
				unbridgedChannel = true
 | 
			
		||||
				t.equal(channel.id, "665310973967597573")
 | 
			
		||||
				t.equal(guildID, "665289423482519565")
 | 
			
		||||
 | 
			
		||||
				// In order to not simulate channel deletion and not trigger the post unlink channels, pre-unlink space check
 | 
			
		||||
				db.prepare("DELETE FROM channel_room WHERE guild_id = '665289423482519565' AND channel_id = '665310973967597573'").run()
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		api: {
 | 
			
		||||
			async *generateFullHierarchy(spaceID) {
 | 
			
		||||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				yield {
 | 
			
		||||
					room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
 | 
			
		||||
					children_state: {},
 | 
			
		||||
					guest_can_join: false,
 | 
			
		||||
					num_joined_members: 2
 | 
			
		||||
				}
 | 
			
		||||
				/* c8 ignore next */
 | 
			
		||||
			},
 | 
			
		||||
 | 
			
		||||
			async setUserPower(spaceID, targetUser, powerLevel) {
 | 
			
		||||
				downgradedPowerLevel = true
 | 
			
		||||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
				t.equal(targetUser, me)
 | 
			
		||||
				t.equal(powerLevel, 0)
 | 
			
		||||
			},
 | 
			
		||||
 | 
			
		||||
			async leaveRoom(spaceID) {
 | 
			
		||||
				leftRoom = true
 | 
			
		||||
				t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.equal(unbridgedChannel, true)
 | 
			
		||||
	t.equal(downgradedPowerLevel, true)
 | 
			
		||||
	t.equal(leftRoom, true)
 | 
			
		||||
 | 
			
		||||
	const missed_row = db.prepare(getLinkRowQuery).get()
 | 
			
		||||
	t.equal(missed_row, undefined)
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,8 @@ const {reg} = require("../src/matrix/read-registration")
 | 
			
		|||
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_name = "cadence.moe"
 | 
			
		||||
reg.ooye.namespace_prefix = "_ooye_"
 | 
			
		||||
reg.sender_localpart = "_ooye_bot"
 | 
			
		||||
reg.id = "baby"
 | 
			
		||||
reg.as_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