mirror of
				https://github.com/Blah-IM/blahrs.git
				synced 2025-10-31 02:31:37 +00:00 
			
		
		
		
	Impl room listing for testing frontend
This commit is contained in:
		
							parent
							
								
									6831c3d25a
								
							
						
					
					
						commit
						57b17547ca
					
				
					 2 changed files with 122 additions and 68 deletions
				
			
		|  | @ -22,24 +22,25 @@ | ||||||
|       .log { |       .log { | ||||||
|         margin: auto; |         margin: auto; | ||||||
|         font-style: italic; |         font-style: italic; | ||||||
|       } |         &::before { | ||||||
|       .log::before { |  | ||||||
|             content: "«"; |             content: "«"; | ||||||
|         } |         } | ||||||
|       .log::after { |         &::after { | ||||||
|             content: "»"; |             content: "»"; | ||||||
|         } |         } | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       #input-area > * { |       #input-area > * { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|       } |         & > label { | ||||||
|       #input-area > * > label { |  | ||||||
|             margin: auto; |             margin: auto; | ||||||
|         } |         } | ||||||
|       #input-area > * > input { |         & > input, | ||||||
|  |         & > select { | ||||||
|             flex: 1; |             flex: 1; | ||||||
|         } |         } | ||||||
|  |       } | ||||||
|     </style> |     </style> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|  | @ -53,17 +54,24 @@ | ||||||
|         <input type="text" id="user-pubkey" placeholder="-" readonly /> |         <input type="text" id="user-pubkey" placeholder="-" readonly /> | ||||||
|         <button id="regen-key">regenerate</button> |         <button id="regen-key">regenerate</button> | ||||||
|       </div> |       </div> | ||||||
| 
 |  | ||||||
|       <div> |       <div> | ||||||
|         <label for="room-url">room url:</label> |         <label for="server-url">server url:</label> | ||||||
|         <input |         <input | ||||||
|             type="url" |             type="url" | ||||||
|             id="room-url" |             id="server-url" | ||||||
|             placeholder="https://example.com" |             placeholder="https://example.com" | ||||||
|             pattern="https://.*" |             pattern="https://.*" | ||||||
|             required |             required | ||||||
|             /> |             /> | ||||||
|         <button id="join-room">join room</button> |       </div> | ||||||
|  |       <div> | ||||||
|  |         <label for="rooms">joined rooms:</label> | ||||||
|  |         <select id="rooms"></select> | ||||||
|  | 
 | ||||||
|  |         <label for="join-new-room">join public room:</label> | ||||||
|  |         <select id="join-new-room"></select> | ||||||
|  | 
 | ||||||
|  |         <button id="refresh-rooms">refresh room list</select> | ||||||
|       </div> |       </div> | ||||||
|       <div> |       <div> | ||||||
|         <label for="chat">chat:</label> |         <label for="chat">chat:</label> | ||||||
|  |  | ||||||
							
								
								
									
										148
									
								
								pages/main.js
									
										
									
									
									
								
							
							
						
						
									
										148
									
								
								pages/main.js
									
										
									
									
									
								
							|  | @ -1,12 +1,13 @@ | ||||||
| const msgFlow = document.querySelector('#msg-flow'); | const msgFlow = document.querySelector('#msg-flow'); | ||||||
| const userPubkeyDisplay = document.querySelector('#user-pubkey'); | const userPubkeyDisplay = document.querySelector('#user-pubkey'); | ||||||
| const roomUrlInput = document.querySelector('#room-url'); | const serverUrlInput = document.querySelector('#server-url'); | ||||||
|  | const roomsInput = document.querySelector('#rooms'); | ||||||
|  | const joinNewRoomInput = document.querySelector('#join-new-room'); | ||||||
| const chatInput = document.querySelector('#chat'); | const chatInput = document.querySelector('#chat'); | ||||||
| const regenKeyBtn = document.querySelector('#regen-key'); | const regenKeyBtn = document.querySelector('#regen-key'); | ||||||
| const joinRoomBtn = document.querySelector('#join-room'); |  | ||||||
| 
 | 
 | ||||||
| let roomUrl = ''; | let serverUrl = null; | ||||||
| let roomUuid = null; | let curRoom = null; | ||||||
| let ws = null; | let ws = null; | ||||||
| let keypair = null; | let keypair = null; | ||||||
| let defaultConfig = {}; | let defaultConfig = {}; | ||||||
|  | @ -65,9 +66,7 @@ async function loadKeypair() { | ||||||
| 
 | 
 | ||||||
| async function generateKeypair() { | async function generateKeypair() { | ||||||
|     log('generating keypair'); |     log('generating keypair'); | ||||||
|     regenKeyBtn.disabled = true; |     document.querySelectorAll('input, button, select').forEach((el) => el.disabled = true); | ||||||
|     chatInput.disabled = true; |  | ||||||
|     joinRoomBtn.disabled = true; |  | ||||||
|     try { |     try { | ||||||
|         keypair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']); |         keypair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  | @ -84,10 +83,7 @@ async function generateKeypair() { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     log('keypair generated'); |     log('keypair generated'); | ||||||
| 
 |     document.querySelectorAll('input, button, select').forEach((el) => el.disabled = false); | ||||||
|     regenKeyBtn.disabled = false; |  | ||||||
|     chatInput.disabled = false; |  | ||||||
|     joinRoomBtn.disabled = false; |  | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         const serialize = (k) => crypto.subtle.exportKey('jwk', k); |         const serialize = (k) => crypto.subtle.exportKey('jwk', k); | ||||||
|  | @ -165,23 +161,21 @@ function escapeHtml(text) { | ||||||
|         .replaceAll("'", '''); |         .replaceAll("'", '''); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function connectRoom(url) { | async function genAuthHeader() { | ||||||
|     if (url === '' || url == roomUrl || keypair === null) return; |     return { | ||||||
|     const match = url.match(/^https?:\/\/.*\/([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})\/?/); |         headers: { | ||||||
|     if (match === null) { |             'Authorization': await signData({ typ: 'auth' }), | ||||||
|         log('invalid room url'); |         }, | ||||||
|         return; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|     roomUrl = url; | async function enterRoom(ruuid) { | ||||||
|     roomUuid = match[1]; |     log(`loading room: ${ruuid}`); | ||||||
|  |     curRoom = ruuid; | ||||||
| 
 | 
 | ||||||
|     log(`fetching room: ${url}`); |     genAuthHeader() | ||||||
| 
 |     .then(opts => fetch(`${serverUrl}/room/${ruuid}`, opts)) | ||||||
|     const genFetchOpts = async () => ({ headers: { 'Authorization': await signData({ typ: 'auth' }) } }); |     .then(async (resp) => [resp.status, await resp.json()]) | ||||||
|     genFetchOpts() |  | ||||||
|     .then(opts => fetch(url, opts)) |  | ||||||
|     .then(async (resp) => { return [resp.status, await resp.json()]; }) |  | ||||||
|     .then(async ([status, json]) => { |     .then(async ([status, json]) => { | ||||||
|         if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); |         if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); | ||||||
|         document.title = `room: ${json.title}` |         document.title = `room: ${json.title}` | ||||||
|  | @ -190,8 +184,8 @@ async function connectRoom(url) { | ||||||
|         log(`failed to get room metadata: ${e}`); |         log(`failed to get room metadata: ${e}`); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     genFetchOpts() |     genAuthHeader() | ||||||
|     .then(opts => fetch(`${url}/item`, opts)) |     .then(opts => fetch(`${serverUrl}/room/${ruuid}/item`, opts)) | ||||||
|     .then(async (resp) => { return [resp.status, await resp.json()]; }) |     .then(async (resp) => { return [resp.status, await resp.json()]; }) | ||||||
|     .then(async ([status, json]) => { |     .then(async ([status, json]) => { | ||||||
|         if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); |         if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); | ||||||
|  | @ -205,24 +199,31 @@ async function connectRoom(url) { | ||||||
|     .catch((e) => { |     .catch((e) => { | ||||||
|         log(`failed to fetch history: ${e}`); |         log(`failed to fetch history: ${e}`); | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     // TODO: There is a time window where events would be lost.
 |  | ||||||
| 
 |  | ||||||
|     await connectWs(); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function connectWs() { | async function connectServer(newServerUrl) { | ||||||
|  |     if (newServerUrl === '' || keypair === null) return; | ||||||
|  |     let wsUrl | ||||||
|  |     try { | ||||||
|  |         wsUrl = new URL(newServerUrl); | ||||||
|  |     } catch (e) { | ||||||
|  |         log(`invalid url: ${e}`); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     serverUrl = newServerUrl; | ||||||
|  | 
 | ||||||
|     if (ws !== null) { |     if (ws !== null) { | ||||||
|         ws.close(); |         ws.close(); | ||||||
|     } |     } | ||||||
|     const wsUrl = new URL(roomUrl); | 
 | ||||||
|  |     log('connecting server'); | ||||||
|     wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:'; |     wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:'; | ||||||
|     wsUrl.pathname = '/ws'; |     wsUrl.pathname = '/ws'; | ||||||
|     ws = new WebSocket(wsUrl); |     ws = new WebSocket(wsUrl); | ||||||
|     ws.onopen = async (_) => { |     ws.onopen = async (_) => { | ||||||
|         const auth = await signData({ typ: 'auth' }); |         const auth = await signData({ typ: 'auth' }); | ||||||
|         await ws.send(auth); |         await ws.send(auth); | ||||||
|         log('listening on events'); |         log(`listening events on server: ${serverUrl}`); | ||||||
|     } |     } | ||||||
|     ws.onclose = (e) => { |     ws.onclose = (e) => { | ||||||
|         console.error(e); |         console.error(e); | ||||||
|  | @ -236,32 +237,69 @@ async function connectWs() { | ||||||
|         console.log('ws event', e.data); |         console.log('ws event', e.data); | ||||||
|         const msg = JSON.parse(e.data); |         const msg = JSON.parse(e.data); | ||||||
|         if (msg.chat !== undefined) { |         if (msg.chat !== undefined) { | ||||||
|             showChatMsg(msg.chat); |             if (msg.chat.signee.payload.room === curRoom) { | ||||||
|  |                 await showChatMsg(msg.chat); | ||||||
|  |             } else { | ||||||
|  |                 console.log('ignore background room item'); | ||||||
|  |             } | ||||||
|         } else if (msg.lagged !== undefined) { |         } else if (msg.lagged !== undefined) { | ||||||
|             log('some events are dropped because of queue overflow') |             log('some events are dropped because of queue overflow') | ||||||
|         } else { |         } else { | ||||||
|             log(`unknown ws message: ${e.data}`); |             log(`unknown ws message: ${e.data}`); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  | 
 | ||||||
|  |     loadRoomList(true); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function joinRoom() { | async function loadRoomList(autoJoin) { | ||||||
|  |     log('loading room list'); | ||||||
|  | 
 | ||||||
|  |     async function loadInto(targetEl, filter) { | ||||||
|         try { |         try { | ||||||
|         joinRoomBtn.disabled = true; |             targetEl.replaceChildren(); | ||||||
|         await signAndPost(`${roomUrl}/admin`, { |             const resp = await fetch(`${serverUrl}/room?filter=${filter}`, await genAuthHeader()) | ||||||
|  |             const json = await resp.json() | ||||||
|  |             if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`); | ||||||
|  |             for (const { ruuid, title, attrs } of json.rooms) { | ||||||
|  |                 const el = document.createElement('option'); | ||||||
|  |                 el.value = ruuid; | ||||||
|  |                 el.innerText = `${title} (uuid=${ruuid}, attrs=${attrs})`; | ||||||
|  |                 targetEl.appendChild(el); | ||||||
|  |             } | ||||||
|  |         } catch (e) { | ||||||
|  |             log(`failed to load room list: ${err}`) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     loadInto(roomsInput, 'joined') | ||||||
|  |     .then(async (_) => { | ||||||
|  |         if (autoJoin && roomsInput.value !== '') { | ||||||
|  |             await enterRoom(roomsInput.value); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     loadInto(joinNewRoomInput, 'public') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function joinRoom(ruuid) { | ||||||
|  |     try { | ||||||
|  |         joinNewRoomInput.disabled = true; | ||||||
|  |         await signAndPost(`${serverUrl}/room/${ruuid}/admin`, { | ||||||
|             // sorted fields.
 |             // sorted fields.
 | ||||||
|             permission: 1, // POST_CHAT
 |             permission: 1, // POST_CHAT
 | ||||||
|             room: roomUuid, |             room: ruuid, | ||||||
|             typ: 'add_member', |             typ: 'add_member', | ||||||
|             user: await getUserPubkey(), |             user: await getUserPubkey(), | ||||||
|         }); |         }); | ||||||
|         log('joined room'); |         log('joined room'); | ||||||
|         await connectWs(); |         await loadRoomList(false) | ||||||
|  |         await enterRoom(ruuid); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|         console.error(e); |         console.error(e); | ||||||
|         log(`failed to join room: ${e}`); |         log(`failed to join room: ${e}`); | ||||||
|     } finally { |     } finally { | ||||||
|         joinRoomBtn.disabled = false; |         joinNewRoomInput.disabled = false; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -302,7 +340,7 @@ async function signData(payload) { | ||||||
| 
 | 
 | ||||||
| async function postChat(text) { | async function postChat(text) { | ||||||
|     text = text.trim(); |     text = text.trim(); | ||||||
|     if (keypair === null || roomUuid === null || text === '') return; |     if (keypair === null || curRoom === null || text === '') return; | ||||||
| 
 | 
 | ||||||
|     chatInput.disabled = true; |     chatInput.disabled = true; | ||||||
| 
 | 
 | ||||||
|  | @ -313,10 +351,10 @@ async function postChat(text) { | ||||||
|         } else { |         } else { | ||||||
|             richText = [text]; |             richText = [text]; | ||||||
|         } |         } | ||||||
|         await signAndPost(`${roomUrl}/item`, { |         await signAndPost(`${serverUrl}/room/${curRoom}/item`, { | ||||||
|             // sorted fields.
 |             // sorted fields.
 | ||||||
|             rich_text: richText, |             rich_text: richText, | ||||||
|             room: roomUuid, |             room: curRoom, | ||||||
|             typ: 'chat', |             typ: 'chat', | ||||||
|         }); |         }); | ||||||
|         chatInput.value = ''; |         chatInput.value = ''; | ||||||
|  | @ -342,13 +380,15 @@ window.onload = async (_) => { | ||||||
|     if (keypair !== null) { |     if (keypair !== null) { | ||||||
|         userPubkeyDisplay.value = await getUserPubkey(); |         userPubkeyDisplay.value = await getUserPubkey(); | ||||||
|     } |     } | ||||||
|     if (roomUrlInput.value === '' && defaultConfig.room_url) { |     if (serverUrlInput.value === '' && defaultConfig.server_url) { | ||||||
|         roomUrlInput.value = defaultConfig.room_url; |         serverUrlInput.value = defaultConfig.server_url; | ||||||
|  |     } | ||||||
|  |     if (serverUrlInput.value !== '') { | ||||||
|  |         await connectServer(serverUrlInput.value); | ||||||
|     } |     } | ||||||
|     await connectRoom(roomUrlInput.value); |  | ||||||
| }; | }; | ||||||
| roomUrlInput.onchange = async (e) => { | serverUrlInput.onchange = async (e) => { | ||||||
|     await connectRoom(e.target.value); |     await connectServer(e.target.value); | ||||||
| }; | }; | ||||||
| chatInput.onkeypress = async (e) => { | chatInput.onkeypress = async (e) => { | ||||||
|     if (e.key === 'Enter') { |     if (e.key === 'Enter') { | ||||||
|  | @ -358,6 +398,12 @@ chatInput.onkeypress = async (e) => { | ||||||
| regenKeyBtn.onclick = async (_) => { | regenKeyBtn.onclick = async (_) => { | ||||||
|     await generateKeypair(); |     await generateKeypair(); | ||||||
| }; | }; | ||||||
| joinRoomBtn.onclick = async (_) => { | roomsInput.onchange = async (_) => { | ||||||
|     await joinRoom(); |     await enterRoom(roomsInput.value); | ||||||
|  | }; | ||||||
|  | joinNewRoomInput.onchange = async (_) => { | ||||||
|  |     await joinRoom(joinNewRoomInput.value); | ||||||
|  | }; | ||||||
|  | document.querySelector('#refresh-rooms').onclick = async (_) => { | ||||||
|  |     await loadRoomList(true); | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 oxalica
						oxalica