UI for linking existing space
This commit is contained in:
		
							parent
							
								
									3d0609f8f1
								
							
						
					
					
						commit
						d45a0bdc10
					
				
					 15 changed files with 293 additions and 116 deletions
				
			
		|  | @ -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. | ||||
| - Event dispatcher will only ensureRoom if the guild_active state is 1. | ||||
| - 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, | ||||
| 	"room_id"	TEXT NOT NULL, | ||||
| 	"type"	TEXT, | ||||
| 	"name"	TEXT, | ||||
| 	"topic" TEXT, | ||||
| 	"avatar"	TEXT, | ||||
| 	PRIMARY KEY("mxid","room_id") | ||||
| ) 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 | ||||
| 		room_id: string | ||||
| 		type: string | null | ||||
| 		name: string | null | ||||
| 		avatar: string | null | ||||
| 	} | ||||
| 
 | ||||
| 	lottie: { | ||||
|  |  | |||
|  | @ -64,7 +64,9 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ | |||
| 			}] | ||||
| 		} | ||||
| 	] | ||||
| }]) | ||||
| }]).catch(e => { | ||||
| 	console.error(e) | ||||
| }) | ||||
| 
 | ||||
| async function dispatchInteraction(interaction) { | ||||
| 	const interactionId = interaction.data.custom_id || interaction.data.name | ||||
|  |  | |||
|  | @ -208,6 +208,26 @@ async 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", | ||||
| /** | ||||
|  * @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}`) { | ||||
| 		// 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) | ||||
| 		const creation = await api.getStateEvent(event.room_id, "m.room.create", "") | ||||
| 		db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type) VALUES (?, ?, ?)").run(event.sender, event.room_id, creation.type || null) | ||||
| 		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) | ||||
| 		if (avatar) utils.getPublicUrlForMxc(avatar) // make sure it's available in the media_proxy allowed URLs
 | ||||
| 	} | ||||
| 
 | ||||
| 	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), {}) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @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} eventID | ||||
|  | @ -404,6 +414,7 @@ module.exports.createRoom = createRoom | |||
| module.exports.joinRoom = joinRoom | ||||
| module.exports.inviteToRoom = inviteToRoom | ||||
| module.exports.leaveRoom = leaveRoom | ||||
| module.exports.leaveRoomWithReason = leaveRoomWithReason | ||||
| module.exports.getEvent = getEvent | ||||
| module.exports.getEventForTimestamp = getEventForTimestamp | ||||
| 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 | ||||
| 	} | ||||
| 
 | ||||
| 	export type M_Space_Child = { | ||||
| 		via?: string[] | ||||
| 		suggested?: boolean | ||||
| 	} | ||||
| 
 | ||||
| 	export type M_Reaction = { | ||||
| 		"m.relates_to": { | ||||
| 			rel_type: "m.annotation" | ||||
|  |  | |||
|  | @ -71,23 +71,24 @@ block body | |||
|     div | ||||
|       != 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 | ||||
|     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 | ||||
|         each row in linkedChannelsWithDetails | ||||
|           tr | ||||
|             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: +matrix(row) | ||||
|         else | ||||
|           tr | ||||
|             td(colspan="3") | ||||
|               .s-empty-state No channels linked between Discord and Matrix yet... | ||||
|     .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.") | ||||
|         input(type="hidden" name="guild_id" value=guild_id) | ||||
|         table.s-table.s-table__bx-simple | ||||
|           each row in linkedChannelsWithDetails | ||||
|             tr | ||||
|               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: +matrix(row) | ||||
|           else | ||||
|             tr | ||||
|               td(colspan="3") | ||||
|                 .s-empty-state No channels linked between Discord and Matrix yet... | ||||
| 
 | ||||
|   h3.mt32.fs-category Auto-create | ||||
|   .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. | ||||
|       - let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get() | ||||
|       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 | ||||
| 
 | ||||
|   h3.mt32.fs-category Privacy level | ||||
|   .s-card | ||||
|     form(hx-post="/api/privacy-level" hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input") | ||||
|   if space_id | ||||
|     h3.mt32.fs-category Privacy level | ||||
|     .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) | ||||
|       .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 | ||||
|         h3.mt24 Channels | ||||
|         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. | ||||
|       div | ||||
|         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. | ||||
|       div | ||||
|         h3.mt24 Unavailable channels: Deleted from Discord | ||||
|         .s-card.p0 | ||||
|           ul.my8.ml24 | ||||
|             each row in removedUncachedChannels | ||||
|               li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`)= row.nick || row.name | ||||
|         h3.mt24 Unavailable channels: Wrong type | ||||
|         .s-card.p0 | ||||
|           ul.my8.ml24 | ||||
|             each row in removedWrongTypeChannels | ||||
|               li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`) (#{row.type}) #{row.name} | ||||
|       div- // Rooms | ||||
|         h3.mt24 Unavailable rooms: Already linked | ||||
|         .s-card.p0 | ||||
|           ul.my8.ml24 | ||||
|             each row in removedLinkedRooms | ||||
|               li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name | ||||
|         h3.mt24 Unavailable rooms: Wrong type | ||||
|         .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 | ||||
|         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 | ||||
|           h3.mt24 Channels | ||||
|           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. | ||||
|         div | ||||
|           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. | ||||
|         div | ||||
|           h3.mt24 Unavailable channels: Deleted from Discord | ||||
|           .s-card.p0 | ||||
|             ul.my8.ml24 | ||||
|               each row in removedUncachedChannels | ||||
|                 li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`)= row.nick || row.name | ||||
|           h3.mt24 Unavailable channels: Wrong type | ||||
|           .s-card.p0 | ||||
|             ul.my8.ml24 | ||||
|               each row in removedWrongTypeChannels | ||||
|                 li: a(href=`https://discord.com/channels/${guild_id}/${row.channel_id}`) (#{row.type}) #{row.name} | ||||
|         div- // Rooms | ||||
|           h3.mt24 Unavailable rooms: Already linked | ||||
|           .s-card.p0 | ||||
|             ul.my8.ml24 | ||||
|               each row in removedLinkedRooms | ||||
|                 li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name | ||||
|           h3.mt24 Unavailable rooms: Wrong type | ||||
|           .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 | ||||
| 
 | ||||
| block body | ||||
|   if !managed | ||||
|   if !session.data.user_id | ||||
|     .s-empty-state.wmx4.p48 | ||||
|       != icons.Spots.SpotEmptyXL | ||||
|       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 {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") | ||||
| 
 | ||||
| /** @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"}) | ||||
| 
 | ||||
| 	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
 | ||||
| })) | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ const {LRUCache} = require("lru-cache") | |||
| const Ty = require("../../types") | ||||
| const uqr = require("uqr") | ||||
| 
 | ||||
| const {discord, as, sync, select} = require("../../passthrough") | ||||
| const {discord, as, sync, select, from, db} = require("../../passthrough") | ||||
| /** @type {import("../pug-sync")} */ | ||||
| const pugSync = sync.require("../pug-sync") | ||||
| /** @type {import("../../d2m/actions/create-space")} */ | ||||
|  | @ -109,15 +109,21 @@ function getChannelRoomsLinks(guildID, rooms) { | |||
| as.router.get("/guild", defineEventHandler(async event => { | ||||
| 	const {guild_id} = await getValidatedQuery(event, schema.guild.parse) | ||||
| 	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
 | ||||
| 	const guild = discord.guilds.get(guild_id) | ||||
| 
 | ||||
| 	// 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}) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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() | ||||
| 	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" $&`) | ||||
| 	assert.notEqual(svg, generatedSvg) | ||||
| 
 | ||||
| 	// Unlinked guild
 | ||||
| 	if (!row) { | ||||
| 	// Easy mode guild that hasn't been linked yet - need to remove elements that would require an existing space
 | ||||
| 	if (!row.space_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
 | ||||
|  |  | |||
|  | @ -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 => { | ||||
| 	const content = await router.test("get", "/guild", { | ||||
| 		sessionData: { | ||||
| 			user_id: "1", | ||||
| 			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 => { | ||||
| 	const content = await router.test("get", "/guild?guild_id=1", { | ||||
| 		sessionData: { | ||||
| 			user_id: "1", | ||||
| 			managedGuilds: [] | ||||
| 		}, | ||||
| 	}) | ||||
|  | @ -43,6 +45,7 @@ test("web invite: access denied with invalid nonce", async t => { | |||
| test("web guild: can view unbridged guild", async t => { | ||||
| 	const content = await router.test("get", "/guild?guild_id=66192955777486848", { | ||||
| 		sessionData: { | ||||
| 			user_id: "1", | ||||
| 			managedGuilds: ["66192955777486848"] | ||||
| 		}, | ||||
| 		api: { | ||||
|  |  | |||
|  | @ -16,6 +16,10 @@ const {reg} = require("../../matrix/read-registration") | |||
| const api = sync.require("../../matrix/api") | ||||
| 
 | ||||
| const schema = { | ||||
| 	linkSpace: z.object({ | ||||
| 		guild_id: z.string(), | ||||
| 		space_id: z.string() | ||||
| 	}), | ||||
| 	link: z.object({ | ||||
| 		guild_id: 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 => { | ||||
| 	const parsedBody = await readValidatedBody(event, schema.link.parse) | ||||
| 	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 DiscordTypes = require("discord-api-types/v10") | ||||
| const dUtils = require("../discord/utils") | ||||
| const reg = require("../matrix/read-registration") | ||||
| 
 | ||||
| const {sync, discord, as, select} = require("../passthrough") | ||||
| /** @type {import("./pug-sync")} */ | ||||
| const pugSync = sync.require("./pug-sync") | ||||
| /** @type {import("../m2d/converters/utils")} */ | ||||
| const mUtils = sync.require("../m2d/converters/utils") | ||||
| const {id} = require("../../addbot") | ||||
| 
 | ||||
| // 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, "/ok", "ok.pug") | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue