bugfixes and style improvements
							parent
							
								
									0b830174d3
								
							
						
					
					
						commit
						05bbdc0d60
					
				
							
								
								
									
										333
									
								
								client/app.js
								
								
								
								
							
							
						
						
									
										333
									
								
								client/app.js
								
								
								
								
							|  | @ -1,93 +1,102 @@ | |||
| import { render, html } from '/uhtml.js'; | ||||
| import { render, html } from "/uhtml.js"; | ||||
| 
 | ||||
| window.currentThreadId = 1; | ||||
| 
 | ||||
| function chooseThread(thread) { | ||||
| 	if (window.currentThreadId) | ||||
| 		document.getElementById(`thread${window.currentThreadId}`).classList.remove('selected'); | ||||
| 	document.getElementById(`thread${thread.id}`).classList.add('selected'); | ||||
| 	window.currentThreadId = thread.id; | ||||
| 	window.earliestMessage = null; | ||||
| 	document.getElementById("messages").innerHTML = ""; | ||||
| 	document.getElementById("threadname").textContent = thread.name; | ||||
| 	loadMessages(); | ||||
|   if (window.currentThreadId) | ||||
|     document | ||||
|       .getElementById(`thread${window.currentThreadId}`) | ||||
|       .classList.remove("active"); | ||||
|   document.getElementById(`thread${thread.id}`).classList.add("active"); | ||||
|   window.currentThreadId = thread.id; | ||||
|   window.earliestMessage = null; | ||||
|   document.getElementById("messages").innerHTML = ""; | ||||
|   document.getElementById("threadname").textContent = thread.name; | ||||
|   if (!thread.permissions.post) { | ||||
|     document.getElementById("msginput").classList.add("hidden"); | ||||
|   } else { | ||||
|     document.getElementById("msginput").classList.remove("hidden"); | ||||
|   } | ||||
|   loadMessages(); | ||||
| } | ||||
| 
 | ||||
| function loadMessages() { | ||||
| 	window.emit("get_history", { | ||||
| 		before: window.earliestMessage, | ||||
| 		thread: window.currentThreadId, | ||||
| 	}, msg => { | ||||
| 		if (msg.messages.length > 0) { | ||||
| 			window.earliestMessage = msg.messages[msg.messages.length - 1].id; | ||||
| 			for (let message of msg.messages) | ||||
| 				document.getElementById("messages").prepend(html.node` | ||||
|   window.emit( | ||||
|     "get_history", | ||||
|     { | ||||
|       before: window.earliestMessage, | ||||
|       thread: window.currentThreadId, | ||||
|     }, | ||||
|     (msg) => { | ||||
|       if (msg.messages.length > 0) { | ||||
|         window.earliestMessage = msg.messages[msg.messages.length - 1].id; | ||||
|         for (let message of msg.messages) | ||||
|           document.getElementById("messages").prepend(html.node` | ||||
| 					<div class='message'> | ||||
| 						<strong>${message.name}: </strong> | ||||
| 						${message.message} | ||||
| 					</div>`); | ||||
| 		} | ||||
| 		if (!msg.more) | ||||
| 			document.getElementById("loadmore").classList.add("hidden"); | ||||
| 		else | ||||
| 			document.getElementById("loadmore").classList.remove("hidden"); | ||||
| 	}); | ||||
|       } | ||||
|       if (!msg.more) | ||||
|         document.getElementById("loadmore").classList.add("hidden"); | ||||
|       else document.getElementById("loadmore").classList.remove("hidden"); | ||||
|     } | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function addThread(thread) { | ||||
| 	let node = html.node` | ||||
| 		<div class='thread' onclick=${() => { | ||||
| 			chooseThread(thread); | ||||
| 			if (!thread.permissions.post) { | ||||
| 				document.getElementById("msginput").classList.add("hidden"); | ||||
| 			} else { | ||||
| 				document.getElementById("msginput").classList.remove("hidden"); | ||||
| 			} | ||||
| 		}}>${thread.name}</div>`; | ||||
| 	node.id = `thread${thread.id}`; | ||||
| 	document.getElementById("threadlist").appendChild(node); | ||||
| function addThread(thread, top) { | ||||
|   let node = html.node` | ||||
| 		<button class='thread tab' onclick=${() => chooseThread(thread)}>${ | ||||
|     thread.name | ||||
|   }</button>`; | ||||
|   node.id = `thread${thread.id}`; | ||||
|   document.getElementById("threadlist")[top ? "prepend" : "appendChild"](node); | ||||
| } | ||||
| 
 | ||||
| function addMember() { | ||||
| 	const name = document.getElementById("membername").value; | ||||
| 	window.threadmembers.push(name); | ||||
| 	document.getElementById("memberlist").appendChild( | ||||
| 		html.node`<p class='member'>${name}</p>`); | ||||
| 	document.getElementById("membername").value = ""; | ||||
|   const name = document.getElementById("membername").value; | ||||
|   window.threadmembers.push(name); | ||||
|   document | ||||
|     .getElementById("memberlist") | ||||
|     .appendChild(html.node`<p class='member'>${name}</p>`); | ||||
|   document.getElementById("membername").value = ""; | ||||
| } | ||||
| 
 | ||||
| async function createThread(e) { | ||||
| 	e.preventDefault(); | ||||
| 	let name = document.getElementById("newthreadname"); | ||||
| 	if (!name.value) { | ||||
| 		name.insertAdjacentHTML('afterend', `<p>name cannot be empty</p>`); | ||||
| 		return; | ||||
| 	} | ||||
| 	let members = window.threadmembers.map(name => ({ name })); | ||||
| 	const perms = document.querySelector( | ||||
| 		'input[name="permissions"]:checked').value; | ||||
| 	if (perms === "private_view") | ||||
| 		members = (await new Promise(resolve => | ||||
| 			window.emit("get_keys", { names: window.threadmembers }, resolve) | ||||
| 			)).keys; | ||||
| 	let permissions; | ||||
| 	if (perms === "public") { | ||||
| 		permissions = { | ||||
| 			view_limited: false, | ||||
| 			post_limited: false | ||||
| 		}; | ||||
| 	} else if (perms === "private_post") { | ||||
| 		permissions = { | ||||
| 			view_limited: false, | ||||
| 			post_limited: true | ||||
| 		}; | ||||
| 	} else if (perms === "private_view") { | ||||
| 		permissions = { | ||||
| 			view_limited: true, | ||||
| 			post_limited: true | ||||
| 		}; | ||||
| 		// generate key
 | ||||
| 		/* wip | ||||
|   e.preventDefault(); | ||||
|   let name = document.getElementById("newthreadname"); | ||||
|   if (!name.value) { | ||||
|     name.insertAdjacentHTML("afterend", `<p>name cannot be empty</p>`); | ||||
|     return; | ||||
|   } | ||||
|   let members = window.threadmembers.map((name) => ({ name })); | ||||
|   const perms = document.querySelector( | ||||
|     'input[name="permissions"]:checked' | ||||
|   ).value; | ||||
|   if (perms === "private_view") | ||||
|     members = ( | ||||
|       await new Promise((resolve) => | ||||
|         window.emit("get_keys", { names: window.threadmembers }, resolve) | ||||
|       ) | ||||
|     ).keys; | ||||
|   let permissions; | ||||
|   if (perms === "public") { | ||||
|     permissions = { | ||||
|       view_limited: false, | ||||
|       post_limited: false, | ||||
|     }; | ||||
|   } else if (perms === "private_post") { | ||||
|     permissions = { | ||||
|       view_limited: false, | ||||
|       post_limited: true, | ||||
|     }; | ||||
|   } else if (perms === "private_view") { | ||||
|     permissions = { | ||||
|       view_limited: true, | ||||
|       post_limited: true, | ||||
|     }; | ||||
|     // generate key
 | ||||
|     /* wip | ||||
| 		var buf = new Uint8Array(32); | ||||
| 		crypto.getRandomValues(buf); | ||||
| 		const key = aesjs.utils.hex.fromBytes(Array.from(buf)); | ||||
|  | @ -100,46 +109,51 @@ async function createThread(e) { | |||
| 			}); | ||||
| 		} | ||||
| 		*/ | ||||
| 	} | ||||
| 	window.emit("create_thread", { | ||||
| 		name: name.value, | ||||
| 		permissions, | ||||
| 		members | ||||
| 	}, msg => { | ||||
| 		chooseThread({ | ||||
| 			name: name.value, | ||||
| 			id: msg.id | ||||
| 		}); | ||||
| 		document.getElementById('createthread').remove(); | ||||
| 		document.getElementById("loadmore").classList.add("hidden"); | ||||
| 		document.getElementById("msginput").classList.remove("hidden"); | ||||
| 	}); | ||||
|   } | ||||
|   window.emit( | ||||
|     "create_thread", | ||||
|     { | ||||
|       name: name.value, | ||||
|       permissions, | ||||
|       members, | ||||
|     }, | ||||
|     (msg) => { | ||||
|       chooseThread({ | ||||
|         name: name.value, | ||||
|         id: msg.id, | ||||
|       }); | ||||
|       // since the form exists, this will perform cleanup
 | ||||
|       newThread(); | ||||
|       document.getElementById("loadmore").classList.add("hidden"); | ||||
|       document.getElementById("msginput").classList.remove("hidden"); | ||||
|     } | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function sendMessage(e) { | ||||
| 	e.preventDefault(); | ||||
| 	const msg = document.getElementById("msg").value; | ||||
| 	if (!msg) | ||||
| 		return; | ||||
| 	window.emit("send_message", { | ||||
| 		message: msg, | ||||
| 		thread: window.currentThreadId | ||||
| 	}); | ||||
| 	document.getElementById("msg").value = ""; | ||||
|   e.preventDefault(); | ||||
|   const msg = document.getElementById("msg").value; | ||||
|   if (!msg) return; | ||||
|   window.emit("send_message", { | ||||
|     message: msg, | ||||
|     thread: window.currentThreadId, | ||||
|   }); | ||||
|   document.getElementById("msg").value = ""; | ||||
| } | ||||
| 
 | ||||
| function newThread(e) { | ||||
| 	let form = document.getElementById('createthread'); | ||||
| 	if (form) { | ||||
| 		form.remove(); | ||||
| 		e.target.textContent = 'create'; | ||||
| 	} | ||||
| 	else { | ||||
| 		window.threadmembers = [ window.name ]; | ||||
| 		document.getElementById('threads').insertAdjacentElement('afterend', html.node` | ||||
| function newThread() { | ||||
|   let form = document.getElementById("createthread"); | ||||
|   if (form) { | ||||
|     form.remove(); | ||||
|     document.getElementById("newthread").textContent = "create"; | ||||
|   } else { | ||||
|     window.threadmembers = [window.name]; | ||||
|     document.getElementById("threads").insertAdjacentElement( | ||||
|       "afterend", | ||||
|       html.node` | ||||
| 			<form id="createthread" class='column' onsubmit=${createThread}> | ||||
| 				<h3>create thread</h3> | ||||
| 				<label for="newthreadname">thread name</label> | ||||
| 				<label for="newthreadname" class="heading">thread name</label> | ||||
| 				<input type="text" id="newthreadname" /> | ||||
| 				<p id='permissions'>thread permissions</p> | ||||
| 				<input type="radio" id="public" name="permissions" value="public" checked /> | ||||
|  | @ -153,77 +167,80 @@ function newThread(e) { | |||
| 				/> | ||||
| 				<label for="private_view">only members can view and post</label | ||||
| 				><br /><br /> | ||||
| 				<span>members</span><br /> | ||||
| 				<label class="heading" for="membername">members</label> | ||||
| 				<input type="text" id="membername" placeholder="username" onkeydown=${(e) => { | ||||
| 					if (e.key == "Enter") { | ||||
| 						e.preventDefault(); | ||||
| 						addMember(); | ||||
| 					} | ||||
| 				}}/> | ||||
|           if (e.key == "Enter") { | ||||
|             e.preventDefault(); | ||||
|             addMember(); | ||||
|           } | ||||
|         }}/> | ||||
| 				<button id="addmember" onclick=${addMember}>add</button> | ||||
| 				<div id="memberlist"> | ||||
| 					<p class='member'>${window.name}</p> | ||||
| 				</div> | ||||
| 				<br /> | ||||
| 				<input id="submitthread" type="submit" value="create" /> | ||||
| 				<button id="submitthread" type="submit">create</button> | ||||
| 			</form> | ||||
| 		`);
 | ||||
| 		e.target.textContent = 'cancel'; | ||||
| 	} | ||||
| 		` | ||||
|     ); | ||||
|     document.getElementById("newthread").textContent = "cancel"; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function switchTab(event){ | ||||
| 	for (let tab of document.querySelectorAll('.tab')) | ||||
| 		tab.classList.remove('active'); | ||||
| 	for (let tab of document.querySelectorAll('.tabcontent')) | ||||
| 		tab.classList.add('hidden'); | ||||
| 	event.target.classList.add('active'); | ||||
| 	document.getElementById(event.target.id.substring(0, event.target.id.length - 3)) | ||||
| 		.classList.remove('hidden'); | ||||
| function switchTab(event) { | ||||
|   for (let tab of document.querySelectorAll(".tab")) | ||||
|     tab.classList.remove("active"); | ||||
|   for (let tab of document.querySelectorAll(".tabcontent")) | ||||
|     tab.classList.add("hidden"); | ||||
|   event.target.classList.add("active"); | ||||
|   document | ||||
|     .getElementById(event.target.id.substring(0, event.target.id.length - 3)) | ||||
|     .classList.remove("hidden"); | ||||
| } | ||||
| 
 | ||||
| render(document.body, html` | ||||
| 	<div id="threads" class="column"> | ||||
| 		<h3>vybe</h3> | ||||
| 		<h4>threads</h4> | ||||
| 		<div id="threadlist">loading...</div> | ||||
| 		<button id='newthread' onclick=${newThread}>create</button> | ||||
| 	</div> | ||||
| 	<div id="thread" class="column"> | ||||
| 		<div id='title'> | ||||
| 			thread: <strong id="threadname">meow</strong> | ||||
| 		</div> | ||||
| 		<button id='messagetab' class='tab active' onclick=${switchTab}>messages</button> | ||||
| 		<button id='spacetab' class='tab' onclick=${switchTab}>space</button> | ||||
| 		<button id="loadmore" class="hidden" onclick=${loadMessages}>load more messages</button> | ||||
| 		<div id='message' class='tabcontent'> | ||||
| 			<div id='messages'> | ||||
| 			</div> | ||||
| 			<form id="msginput" onsubmit=${sendMessage}> | ||||
| 				<input type="text" placeholder="write a message..." id="msg" /> | ||||
| 				<button type="submit" class="hidden" id="sendmsg"></button> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 		<div id='space' class='tabcontent'></div> | ||||
| 	</div> | ||||
| `);
 | ||||
| render( | ||||
|   document.body, | ||||
|   html` | ||||
|     <div id="threads" class="column"> | ||||
|       <h3>vybe</h3> | ||||
|       <h4>threads</h4> | ||||
|       <div id="threadlist">loading...</div> | ||||
|       <button id="newthread" onclick=${newThread}>create</button> | ||||
|     </div> | ||||
|     <div id="thread" class="column"> | ||||
|       <div id="title">thread: <strong id="threadname">meow</strong></div> | ||||
|       <button id="messagetab" class="tab active" onclick=${switchTab}> | ||||
|         messages | ||||
|       </button> | ||||
|       <button id="spacetab" class="tab" onclick=${switchTab}>space</button> | ||||
|       <button id="loadmore" class="hidden" onclick=${loadMessages}> | ||||
|         load more messages | ||||
|       </button> | ||||
|       <div id="message" class="tabcontent"> | ||||
|         <div id="messages"></div> | ||||
|         <form id="msginput" onsubmit=${sendMessage}> | ||||
|           <input type="text" placeholder="write a message..." id="msg" /> | ||||
|           <button type="submit" class="hidden" id="sendmsg"></button> | ||||
|         </form> | ||||
|       </div> | ||||
|       <div id="space" class="tabcontent"></div> | ||||
|     </div> | ||||
|   ` | ||||
| ); | ||||
| 
 | ||||
| window.socket.on("new_message", (msg) => { | ||||
| 	if (msg.thread !== window.currentThreadId) | ||||
| 		return; | ||||
| 	document.getElementById("messages").appendChild(html.node` | ||||
|   if (msg.thread !== window.currentThreadId) return; | ||||
|   document.getElementById("messages").appendChild(html.node` | ||||
| 		<div class='message'> | ||||
| 			<strong>${msg.name}: </strong> | ||||
| 			${msg.message} | ||||
| 		</div>`); | ||||
| 	if (!window.earliestMessage) | ||||
| 		window.earliestMessage = msg.id; | ||||
|   if (!window.earliestMessage) window.earliestMessage = msg.id; | ||||
| }); | ||||
| window.socket.on("new_thread", addThread); | ||||
| window.socket.on("new_thread", (thread) => addThread(thread, true)); | ||||
| 
 | ||||
| window.emit("list_threads", {}, msg => { | ||||
| 	document.getElementById("threadlist").innerHTML = ""; | ||||
| 	for (let thread of msg.threads) | ||||
| 		addThread(thread); | ||||
| 	chooseThread(msg.threads[0]); | ||||
| window.emit("list_threads", {}, (msg) => { | ||||
|   document.getElementById("threadlist").innerHTML = ""; | ||||
|   for (let thread of msg.threads) addThread(thread); | ||||
|   chooseThread(msg.threads[0]); | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										186
									
								
								client/auth.js
								
								
								
								
							
							
						
						
									
										186
									
								
								client/auth.js
								
								
								
								
							|  | @ -1,90 +1,120 @@ | |||
| import { render, html } from '/uhtml.js'; | ||||
| import { render, html } from "/uhtml.js"; | ||||
| 
 | ||||
| function rand() { | ||||
| 	let str = ""; | ||||
| 	const lookups = | ||||
| 		"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); | ||||
| 	while (str.length < 16) { | ||||
| 		const n = Math.random() * lookups.length; | ||||
| 		str += lookups[Math.floor(n)]; | ||||
| 	} | ||||
| 	return str; | ||||
|   let str = ""; | ||||
|   const lookups = | ||||
|     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); | ||||
|   while (str.length < 16) { | ||||
|     const n = Math.random() * lookups.length; | ||||
|     str += lookups[Math.floor(n)]; | ||||
|   } | ||||
|   return str; | ||||
| } | ||||
| 
 | ||||
| async function auth() { | ||||
| 	const sig = await openpgp.sign({ | ||||
| 		message: new openpgp.CleartextMessage("vybe_auth " + window.session, ""), | ||||
| 		signingKeys: window.keys.priv, | ||||
| 	}); | ||||
| 	window.socket.emit("authenticate", { name: window.name, message: sig }, | ||||
| 		msg => { | ||||
| 			if (!msg.success) { | ||||
| 				console.log('authenticate failed'); | ||||
| 				return; | ||||
| 			} | ||||
| 			document.getElementById("register").remove(); | ||||
| 			import('/app.js'); | ||||
| 		}); | ||||
|   const sig = await openpgp.sign({ | ||||
|     message: new openpgp.CleartextMessage("vybe_auth " + window.session, ""), | ||||
|     signingKeys: window.keys.priv, | ||||
|   }); | ||||
|   window.socket.emit( | ||||
|     "authenticate", | ||||
|     { name: window.name, message: sig }, | ||||
|     (msg) => { | ||||
|       if (!msg.success) { | ||||
|         console.log("authenticate failed", msg); | ||||
|         return; | ||||
|       } | ||||
|       if (document.getElementById("register")) { | ||||
|         document.getElementById("register").remove(); | ||||
|         import("/app.js"); | ||||
|       } | ||||
|     } | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| render(document.body, html` | ||||
| 	<div id="register" class='hidden'> | ||||
| 		<h1>welcome to vybe</h1> | ||||
| 		<h3>a communication network (beta)</h3> | ||||
| 		<p> | ||||
| 			to get started, you'll need an account. we use public key cryptography | ||||
| 			for security, rather than passwords. your keys are stored in your | ||||
| 			browser storage only, so do this on a browser you can access again. | ||||
| 		</p> | ||||
| 		<form onsubmit=${async e => { | ||||
| 			e.preventDefault(); | ||||
| 			const name = document.getElementById("name").value; | ||||
| 			if (!name) | ||||
| 				return; | ||||
| 			const keys = await openpgp.generateKey({ | ||||
| 				userIDs: [{ name }], | ||||
| 			}); | ||||
| 			const priv = await openpgp.readKey({ armoredKey: keys.privateKey }); | ||||
| 			const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); | ||||
| 			window.emit("create_user", { name, pubkey: keys.publicKey }, msg => { | ||||
| 				if (!msg.success) { | ||||
| 					document.querySelector('#registerform').insertAdjacentHTML('afterend', ` | ||||
| 						<p>${msg.message}</p>`); | ||||
| 					return; | ||||
| 				} | ||||
| 				window.keys = { priv, pub }; | ||||
| 				localStorage.setItem("keys", JSON.stringify(keys)); | ||||
| 				localStorage.setItem("name", name); | ||||
| 				window.name = name; | ||||
| 				auth(); | ||||
| 			}); | ||||
| 		}} id="registerform"> | ||||
| 			<label for="name">username: </label> | ||||
| 			<input id="name" type="text" /> | ||||
| 			<input id="submit" type="submit" value='generate keys & register'> | ||||
| 		</form> | ||||
| 	</div> | ||||
| `);
 | ||||
| render( | ||||
|   document.body, | ||||
|   html` | ||||
|     <div id="register" class="hidden"> | ||||
|       <h1>welcome to vybe</h1> | ||||
|       <h3>a communication network (beta)</h3> | ||||
|       <p> | ||||
|         to get started, you'll need an account. we use public key cryptography | ||||
|         for security, rather than passwords. your keys are stored in your | ||||
|         browser storage only, so do this on a browser you can access again. | ||||
|       </p> | ||||
|       <form | ||||
|         onsubmit=${async (e) => { | ||||
|           e.preventDefault(); | ||||
|           const name = document.getElementById("name").value; | ||||
|           if (!name) return; | ||||
|           const keys = await openpgp.generateKey({ | ||||
|             userIDs: [{ name }], | ||||
|           }); | ||||
|           const priv = await openpgp.readKey({ armoredKey: keys.privateKey }); | ||||
|           const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); | ||||
|           window.emit( | ||||
|             "create_user", | ||||
|             { name, pubkey: keys.publicKey }, | ||||
|             (msg) => { | ||||
|               if (!msg.success) { | ||||
|                 document.querySelector("#registerform").insertAdjacentHTML( | ||||
|                   "afterend", | ||||
|                   ` | ||||
| 						<p>${msg.message}</p>` | ||||
|                 ); | ||||
|                 return; | ||||
|               } | ||||
|               window.keys = { priv, pub }; | ||||
|               localStorage.setItem("keys", JSON.stringify(keys)); | ||||
|               localStorage.setItem("name", name); | ||||
|               window.name = name; | ||||
|               auth(); | ||||
|             } | ||||
|           ); | ||||
|         }} | ||||
|         id="registerform" | ||||
|       > | ||||
|         <label for="name">username: </label> | ||||
|         <input id="name" type="text" /> | ||||
|         <input id="submit" type="submit" value="generate keys & register" /> | ||||
|       </form> | ||||
|     </div> | ||||
|   ` | ||||
| ); | ||||
| 
 | ||||
| const gensession = async () => { | ||||
|   window.session = rand(); | ||||
| 
 | ||||
|   window.emit = (type, data, callback) => | ||||
|     window.socket.emit( | ||||
|       type, | ||||
|       { | ||||
|         ...data, | ||||
|         __session: window.session, | ||||
|       }, | ||||
|       callback | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| window.onload = async () => { | ||||
| 	window.socket = io(); | ||||
|   window.socket = io(); | ||||
| 
 | ||||
| 	window.session = rand(); | ||||
| 	window.emit = (type, data, callback) => window.socket.emit(type, { | ||||
| 		...data, | ||||
| 		__session: window.session, | ||||
| 	}, callback); | ||||
|   await gensession(); | ||||
| 
 | ||||
| 	let keys = localStorage.getItem("keys"); | ||||
| 	if (keys) { | ||||
| 		window.name = localStorage.getItem("name"); | ||||
| 		keys = JSON.parse(keys); | ||||
| 		window.keys = { | ||||
| 			priv: await openpgp.readKey({ armoredKey: keys.privateKey }), | ||||
| 			pub: await openpgp.readKey({ armoredKey: keys.publicKey }) | ||||
| 		}; | ||||
| 		await auth(); | ||||
| 	} | ||||
| 	else | ||||
| 		document.getElementById('register').classList.remove('hidden'); | ||||
|   let keys = localStorage.getItem("keys"); | ||||
|   if (keys) { | ||||
|     window.name = localStorage.getItem("name"); | ||||
|     keys = JSON.parse(keys); | ||||
|     window.keys = { | ||||
|       priv: await openpgp.readKey({ armoredKey: keys.privateKey }), | ||||
|       pub: await openpgp.readKey({ armoredKey: keys.publicKey }), | ||||
|     }; | ||||
|     await auth(); | ||||
|   } else document.getElementById("register").classList.remove("hidden"); | ||||
| 
 | ||||
|   window.socket.io.on("reconnect", async (attempt) => { | ||||
|     await gensession(); | ||||
|     if (localStorage.getItem("keys")) await auth(); | ||||
|   }); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,102 +1,124 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| 	<head> | ||||
| 		<script src="/openpgp.min.js"></script> | ||||
| 		<script src="/socket.io.min.v4.6.1.js"></script> | ||||
| 		<script src="/aes.js"></script> | ||||
| 		<script type="module" src="/auth.js"></script> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
| 		<title>vybe</title> | ||||
| 		<style> | ||||
| 			* { | ||||
| 				font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", | ||||
| 					Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", | ||||
| 					sans-serif; | ||||
| 			} | ||||
| 			body, | ||||
| 			button, | ||||
| 			input { | ||||
| 				background: #020202; | ||||
| 				color: #eaeaea; | ||||
| 			} | ||||
| 			body { | ||||
| 				display: flex; | ||||
| 				align-items: stretch; | ||||
| 				margin: 0; | ||||
| 				min-width: min-content; | ||||
| 			} | ||||
| 			h3, h4 { | ||||
| 				margin: 10px 0; | ||||
| 			} | ||||
| 			button { | ||||
| 				border-color: #767676; | ||||
| 			} | ||||
| 			.hidden { | ||||
| 				display: none; | ||||
| 			} | ||||
| 			.column { | ||||
| 				flex: 1; | ||||
| 				margin: 5px; | ||||
| 				overflow: hidden; | ||||
| 			} | ||||
| 			#threads { | ||||
| 				max-width: 250px; | ||||
| 			} | ||||
| 			.thread.selected { | ||||
| 				background-color: #4f4f4f; | ||||
| 			} | ||||
| 			.thread:hover { | ||||
| 				background-color: #3b3b3b; | ||||
| 			} | ||||
| 			#newthread { | ||||
| 				margin-top: 5px; | ||||
| 			} | ||||
| 			#createthread { | ||||
| 				max-width: 350px; | ||||
| 			} | ||||
| 			#permissions { | ||||
| 				margin-bottom: 5px; | ||||
| 			} | ||||
| 			#title { | ||||
| 				margin: 4px 2px; | ||||
| 			} | ||||
| 			.tab { | ||||
| 				border: 0; | ||||
| 				margin-top: 2px; | ||||
| 				padding: 5px 7px; | ||||
| 				color: #ccc; | ||||
| 				font-weight: 500; | ||||
| 			} | ||||
| 			.tab.active { | ||||
| 				background-color: #4f4f4f; | ||||
| 				color: #fff; | ||||
| 			} | ||||
| 			.tab:hover { | ||||
| 				background-color:#3b3b3b; | ||||
| 			} | ||||
| 			#messages { | ||||
| 				margin: 4px 2px; | ||||
| 			} | ||||
| 			#msginput { | ||||
| 				margin-top: 15px; | ||||
| 			} | ||||
| 			.message { | ||||
| 				margin-bottom: 5px; | ||||
| 				overflow-wrap: anywhere; | ||||
| 			} | ||||
| 			#loadmore { | ||||
| 				margin-bottom: 10px; | ||||
| 			} | ||||
| 			.channel { | ||||
| 				font-weight: normal; | ||||
| 			} | ||||
| 			.member { | ||||
| 				margin: 5px 0; | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 	</body> | ||||
|   <head> | ||||
|     <script src="/openpgp.min.js"></script> | ||||
|     <script src="/socket.io.min.v4.6.1.js"></script> | ||||
|     <script src="/aes.js"></script> | ||||
|     <script type="module" src="/auth.js"></script> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>vybe</title> | ||||
|     <style> | ||||
|       * { | ||||
|         font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", | ||||
|           Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", | ||||
|           sans-serif; | ||||
|       } | ||||
|       body, | ||||
|       button, | ||||
|       input { | ||||
|         color: #eaeaea; | ||||
|         border: none; | ||||
|         outline: none; | ||||
|       } | ||||
|       body { | ||||
|         background: #020202; | ||||
|         display: flex; | ||||
|         align-items: stretch; | ||||
|         margin: 0; | ||||
|         min-width: min-content; | ||||
|       } | ||||
|       button, | ||||
|       input, | ||||
|       .tab { | ||||
|         padding: 5px 7px; | ||||
|       } | ||||
|       button { | ||||
|         background: #4f4f4f; | ||||
|       } | ||||
|       input { | ||||
|         background: #2f2f2f; | ||||
|         border-bottom: 2px solid transparent; | ||||
|         padding-bottom: 3px; | ||||
|       } | ||||
|       input:focus { | ||||
|         border-bottom: 2px solid #4f4f4f; | ||||
|       } | ||||
|       input::placeholder { | ||||
|         color: #aaa; | ||||
|       } | ||||
|       button:hover, | ||||
|       .tab:hover { | ||||
|         background-color: #3b3b3b; | ||||
|       } | ||||
|       label.heading { | ||||
|         margin-bottom: 5px; | ||||
|         display: block; | ||||
|       } | ||||
|       h3, | ||||
|       h4 { | ||||
|         margin: 10px 0; | ||||
|       } | ||||
|       .hidden { | ||||
|         display: none; | ||||
|       } | ||||
|       .column { | ||||
|         flex: 1; | ||||
|         margin: 5px; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|       #threads { | ||||
|         max-width: 250px; | ||||
|       } | ||||
|       .thread { | ||||
|         display: block; | ||||
|         width: 100%; | ||||
|         text-align: left; | ||||
|       } | ||||
|       #newthread { | ||||
|         margin-top: 5px; | ||||
|       } | ||||
|       #createthread { | ||||
|         max-width: 350px; | ||||
|       } | ||||
|       #permissions { | ||||
|         margin-bottom: 5px; | ||||
|       } | ||||
|       #title { | ||||
|         margin: 4px 2px; | ||||
|       } | ||||
|       .tab { | ||||
|         background: transparent; | ||||
|         border: 0; | ||||
|         margin-top: 2px; | ||||
|         color: #ccc; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|       .tab.active { | ||||
|         background-color: #4f4f4f; | ||||
|         color: #fff; | ||||
|       } | ||||
|       #messages { | ||||
|         margin: 4px 2px; | ||||
|       } | ||||
|       #msginput { | ||||
|         margin-top: 15px; | ||||
|       } | ||||
|       .message { | ||||
|         margin-bottom: 5px; | ||||
|         overflow-wrap: anywhere; | ||||
|       } | ||||
|       #loadmore { | ||||
|         margin-bottom: 10px; | ||||
|       } | ||||
|       .channel { | ||||
|         font-weight: normal; | ||||
|       } | ||||
|       .member { | ||||
|         margin: 5px 0; | ||||
|       } | ||||
|     </style> | ||||
|   </head> | ||||
|   <body></body> | ||||
| </html> | ||||
|  |  | |||
							
								
								
									
										47
									
								
								index.js
								
								
								
								
							
							
						
						
									
										47
									
								
								index.js
								
								
								
								
							|  | @ -3,23 +3,25 @@ const http = require("http"); | |||
| const { Server } = require("socket.io"); | ||||
| const compression = require("compression"); | ||||
| 
 | ||||
| const events = Object.fromEntries([ | ||||
| 	'create_user', | ||||
| 	'get_history', | ||||
| 	'send_message', | ||||
| 	'authenticate', | ||||
| 	'create_thread', | ||||
| 	'list_threads', | ||||
| 	'get_keys', | ||||
| ].map(event => [event, require('./src/' + event)])); | ||||
| const events = Object.fromEntries( | ||||
|   [ | ||||
|     "create_user", | ||||
|     "get_history", | ||||
|     "send_message", | ||||
|     "authenticate", | ||||
|     "create_thread", | ||||
|     "list_threads", | ||||
|     "get_keys", | ||||
|   ].map((event) => [event, require("./src/" + event)]) | ||||
| ); | ||||
| 
 | ||||
| const app = express(); | ||||
| app.use(compression()); | ||||
| const server = http.createServer(app); | ||||
| const io = new Server(server, { | ||||
| 	cors: { | ||||
| 		origin: true, | ||||
| 	}, | ||||
|   cors: { | ||||
|     origin: true, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const PORT = process.env.PORT || 3435; | ||||
|  | @ -27,20 +29,19 @@ const PORT = process.env.PORT || 3435; | |||
| io.cache = {}; | ||||
| 
 | ||||
| io.on("connection", (socket) => { | ||||
| 	for (let event in events) { | ||||
| 		socket.on(event, (msg, callback) => | ||||
| 			events[event](msg, callback, socket, io) | ||||
| 		); | ||||
| 	} | ||||
| 	socket.on('disconnect', reason => { | ||||
| 		let sockets = io.cache[socket.username]; | ||||
| 		if (sockets) | ||||
| 			sockets.splice(sockets.indexOf(socket.id), 1); | ||||
| 	}) | ||||
|   for (let event in events) { | ||||
|     socket.on(event, (msg, callback) => | ||||
|       events[event](msg, callback, socket, io) | ||||
|     ); | ||||
|   } | ||||
|   socket.on("disconnect", (reason) => { | ||||
|     let sockets = io.cache[socket.username]; | ||||
|     if (sockets) sockets.splice(sockets.indexOf(socket.id), 1); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| server.listen(PORT, () => { | ||||
| 	console.log("server running on port " + PORT); | ||||
|   console.log("server running on port " + PORT); | ||||
| }); | ||||
| 
 | ||||
| app.use(express.static("client")); | ||||
|  |  | |||
|  | @ -2,108 +2,107 @@ const db = require("../db"); | |||
| const authwrap = require("./authwrap"); | ||||
| 
 | ||||
| const create_thread = async (msg, respond, socket, io) => { | ||||
| 	// validate inputs
 | ||||
| 	if (typeof msg.name !== 'string') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "thread name required", | ||||
| 		}); | ||||
| 	} | ||||
| 	if (msg.name.length > 200) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "thread name 200 chars max", | ||||
| 		}); | ||||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		"insert into threads (name, creator) values (?, ?) returning id", | ||||
| 		[msg.name, msg.auth_user.id] | ||||
| 	); | ||||
| 	const thread_id = insert.rows[0].id; | ||||
| 	// set up permissions
 | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		await db.query( | ||||
| 			`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
|   // validate inputs
 | ||||
|   if (typeof msg.name !== "string") { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "thread name required", | ||||
|     }); | ||||
|   } | ||||
|   if (msg.name.length > 200) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "thread name 200 chars max", | ||||
|     }); | ||||
|   } | ||||
|   // add to db
 | ||||
|   const insert = await db.query( | ||||
|     "insert into threads (name, creator) values (?, ?) returning id", | ||||
|     [msg.name, msg.auth_user.id] | ||||
|   ); | ||||
|   const thread_id = insert.rows[0].id; | ||||
|   // set up permissions
 | ||||
|   if (!msg.permissions || !msg.permissions.view_limited) { | ||||
|     await db.query( | ||||
|       `insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, "everyone", false, "view", "true"] | ||||
| 		); | ||||
| 		if (!msg.permissions || !msg.permissions.post_limited) { | ||||
| 			await db.query( | ||||
| 				`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
|       [thread_id, "everyone", false, "view", "true"] | ||||
|     ); | ||||
|     if (!msg.permissions || !msg.permissions.post_limited) { | ||||
|       await db.query( | ||||
|         `insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, "everyone", false, "post", "true"] | ||||
| 			); | ||||
| 		} else { | ||||
| 			await db.query( | ||||
| 				`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
|         [thread_id, "everyone", false, "post", "true"] | ||||
|       ); | ||||
|     } else { | ||||
|       await db.query( | ||||
|         `insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, "members", false, "post", "true"] | ||||
| 			); | ||||
| 		} | ||||
| 	} else { | ||||
| 		await db.query( | ||||
| 			`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
|         [thread_id, "members", false, "post", "true"] | ||||
|       ); | ||||
|     } | ||||
|   } else { | ||||
|     await db.query( | ||||
|       `insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, "members", false, "view", "true"] | ||||
| 		); | ||||
| 		await db.query( | ||||
| 			`insert into permissions (thread, type, mutable, permission, value) 
 | ||||
|       [thread_id, "members", false, "view", "true"] | ||||
|     ); | ||||
|     await db.query( | ||||
|       `insert into permissions (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, "members", false, "post", "true"] | ||||
| 		); | ||||
| 	} | ||||
| 	// add members
 | ||||
| 	for (let user of msg.members) { | ||||
| 		if (!user) continue; | ||||
| 		// get user id
 | ||||
| 		const id = await db.query("select id from users where name = ?", [ | ||||
| 			user.name, | ||||
| 		]); | ||||
| 		if (id.rows.length > 0) { | ||||
| 			const user_id = id.rows[0].id; | ||||
| 			await db.query( | ||||
| 				"insert into members (thread, user, key_delivery) values (?, ?, ?)", | ||||
| 				[thread_id, user_id, user.key] | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		for (let username in io.cache) { | ||||
| 			for (let socket of io.cache[username]) { | ||||
| 				io.to(socket).emit("new_thread", { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
| 					permissions: { | ||||
| 						is_member: false, | ||||
| 						view: true, | ||||
| 						post: !msg.permissions || !msg.permissions.post_limited | ||||
| 					}, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	else { | ||||
| 		for (let member of msg.members) { | ||||
| 			for (let socket of io.cache[member.user]) { | ||||
| 				io.to(socket).emit("new_thread", { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
| 					permissions: { | ||||
| 						is_member: true, | ||||
| 						view: true, | ||||
| 						post: true | ||||
| 					}, | ||||
| 					key: member.key | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		id: insert.rows[0].id, | ||||
| 	}); | ||||
|       [thread_id, "members", false, "post", "true"] | ||||
|     ); | ||||
|   } | ||||
|   // add members
 | ||||
|   for (let user of msg.members) { | ||||
|     if (!user) continue; | ||||
|     // get user id
 | ||||
|     const id = await db.query("select id from users where name = ?", [ | ||||
|       user.name, | ||||
|     ]); | ||||
|     if (id.rows.length > 0) { | ||||
|       const user_id = id.rows[0].id; | ||||
|       await db.query( | ||||
|         "insert into members (thread, user, key_delivery) values (?, ?, ?)", | ||||
|         [thread_id, user_id, user.key] | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|   if (!msg.permissions || !msg.permissions.view_limited) { | ||||
|     for (let username in io.cache) { | ||||
|       for (let socket of io.cache[username]) { | ||||
|         io.to(socket).emit("new_thread", { | ||||
|           name: msg.name, | ||||
|           id: insert.rows[0].id, | ||||
|           permissions: { | ||||
|             is_member: false, | ||||
|             view: true, | ||||
|             post: !msg.permissions || !msg.permissions.post_limited, | ||||
|           }, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } else { | ||||
|     for (let member of msg.members) { | ||||
|       for (let socket of io.cache[member.name]) { | ||||
|         io.to(socket).emit("new_thread", { | ||||
|           name: msg.name, | ||||
|           id: insert.rows[0].id, | ||||
|           permissions: { | ||||
|             is_member: true, | ||||
|             view: true, | ||||
|             post: true, | ||||
|           }, | ||||
|           key: member.key, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   // respond
 | ||||
|   return respond({ | ||||
|     success: true, | ||||
|     id: insert.rows[0].id, | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(create_thread); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue