Merge branch 'main' of http://git.jerl.zone/jerl/vybe
						commit
						f347e659e9
					
				
							
								
								
									
										136
									
								
								client/app.js
								
								
								
								
							
							
						
						
									
										136
									
								
								client/app.js
								
								
								
								
							|  | @ -4,55 +4,62 @@ 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'); | ||||
| 		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) { | ||||
| function addThread(thread, top) { | ||||
| 	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>`; | ||||
| 		<div class='thread' onclick=${() => chooseThread(thread)}>${ | ||||
| 		thread.name | ||||
| 	}</div>`; | ||||
| 	node.id = `thread${thread.id}`; | ||||
| 	document.getElementById("threadlist").appendChild(node); | ||||
| 	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("memberlist") | ||||
| 		.appendChild(html.node`<p class='member'>${name}</p>`); | ||||
| 	document.getElementById("membername").value = ""; | ||||
| } | ||||
| 
 | ||||
|  | @ -65,11 +72,14 @@ async function createThread(e) { | |||
| 	} | ||||
| 	let members = window.threadmembers.map(name => ({ name })); | ||||
| 	const perms = document.querySelector( | ||||
| 		'input[name="permissions"]:checked').value; | ||||
| 		'input[name="permissions"]:checked' | ||||
| 	).value; | ||||
| 	if (perms === "private_view") | ||||
| 		members = (await new Promise(resolve => | ||||
| 			window.emit("get_keys", { names: window.threadmembers }, resolve) | ||||
| 			)).keys; | ||||
| 		members = ( | ||||
| 			await new Promise(resolve => | ||||
| 				window.emit("get_keys", { names: window.threadmembers }, resolve) | ||||
| 			) | ||||
| 		).keys; | ||||
| 	let permissions; | ||||
| 	if (perms === "public") { | ||||
| 		permissions = { | ||||
|  | @ -101,19 +111,24 @@ async function createThread(e) { | |||
| 		} | ||||
| 		*/ | ||||
| 	} | ||||
| 	window.emit("create_thread", { | ||||
| 		name: name.value, | ||||
| 		permissions, | ||||
| 		members | ||||
| 	}, msg => { | ||||
| 		chooseThread({ | ||||
| 	window.emit( | ||||
| 		"create_thread", | ||||
| 		{ | ||||
| 			name: name.value, | ||||
| 			id: msg.id | ||||
| 		}); | ||||
| 		document.getElementById('createthread').remove(); | ||||
| 		document.getElementById("loadmore").classList.add("hidden"); | ||||
| 		document.getElementById("msginput").classList.remove("hidden"); | ||||
| 	}); | ||||
| 			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) { | ||||
|  | @ -128,18 +143,17 @@ function sendMessage(e) { | |||
| 	document.getElementById("msg").value = ""; | ||||
| } | ||||
| 
 | ||||
| function newThread(e) { | ||||
| function newThread() { | ||||
| 	let form = document.getElementById('createthread'); | ||||
| 	if (form) { | ||||
| 		form.remove(); | ||||
| 		e.target.textContent = 'create'; | ||||
| 	} | ||||
| 	else { | ||||
| 		window.threadmembers = [ window.name ]; | ||||
| 		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,7 +167,7 @@ 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(); | ||||
|  | @ -165,20 +179,22 @@ function newThread(e) { | |||
| 					<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){ | ||||
| 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)) | ||||
| 	document | ||||
| 		.getElementById(event.target.id.substring(0, event.target.id.length - 3)) | ||||
| 		.classList.remove('hidden'); | ||||
| } | ||||
| 
 | ||||
|  | @ -194,10 +210,14 @@ render(document.body, html` | |||
| 			thread: <strong id="threadname">meow</strong> | ||||
| 		</div> | ||||
| 		<div id='tabs'> | ||||
| 			<button id='messagetab' class='tab active' onclick=${switchTab}>messages</button><button id='spacetab' class='tab' onclick=${switchTab}>space</button> | ||||
| 			<button id='messagetab' class='tab active' onclick=${switchTab}> | ||||
| 			messages | ||||
| 			</button><button id='spacetab' class='tab' onclick=${switchTab}>space</button> | ||||
| 		</div> | ||||
| 		<div id='message' class='tabcontent'> | ||||
| 			<button id="loadmore" class="hidden" onclick=${loadMessages}>load more messages</button> | ||||
| 		<button id="loadmore" class="hidden" onclick=${loadMessages}> | ||||
| 		load more messages | ||||
| 		</button> | ||||
| 			<div id='messages'></div> | ||||
| 			<form id="msginput" onsubmit=${sendMessage}> | ||||
| 				<input type="text" placeholder="write a message..." id="msg" /> | ||||
|  | @ -219,7 +239,7 @@ window.socket.on("new_message", (msg) => { | |||
| 	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 = ""; | ||||
|  |  | |||
							
								
								
									
										130
									
								
								client/auth.js
								
								
								
								
							
							
						
						
									
										130
									
								
								client/auth.js
								
								
								
								
							|  | @ -16,64 +16,92 @@ async function auth() { | |||
| 		message: new openpgp.CleartextMessage("vybe_auth " + window.session, ""), | ||||
| 		signingKeys: window.keys.priv, | ||||
| 	}); | ||||
| 	window.socket.emit("authenticate", { name: window.name, message: sig }, | ||||
| 	window.socket.emit( | ||||
| 		"authenticate", | ||||
| 		{ name: window.name, message: sig }, | ||||
| 		msg => { | ||||
| 			if (!msg.success) { | ||||
| 				console.log('authenticate failed'); | ||||
| 				console.log('authenticate failed', msg); | ||||
| 				return; | ||||
| 			} | ||||
| 			document.getElementById("register").remove(); | ||||
| 			import('/app.js'); | ||||
| 		}); | ||||
| 			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.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) { | ||||
|  | @ -87,4 +115,10 @@ window.onload = async () => { | |||
| 	} | ||||
| 	else | ||||
| 		document.getElementById('register').classList.remove('hidden'); | ||||
| 
 | ||||
| 	window.socket.io.on("reconnect", async attempt => { | ||||
| 		await gensession(); | ||||
| 		if (localStorage.getItem("keys")) | ||||
| 			await auth(); | ||||
| 	}); | ||||
| }; | ||||
|  |  | |||
|  | @ -12,29 +12,66 @@ | |||
| 		<style> | ||||
| 			* { | ||||
| 				font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", | ||||
| 					Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", | ||||
| 					sans-serif; | ||||
| 				Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", | ||||
| 				sans-serif; | ||||
| 			} | ||||
| 			body, | ||||
| 			button, | ||||
| 			input { | ||||
| 				background: #020202; | ||||
| 				color: #eaeaea; | ||||
| 				border: none; | ||||
| 				outline: none; | ||||
| 			} | ||||
| 			html { | ||||
| 				height: 100%; | ||||
| 			} | ||||
| 			body { | ||||
| 				height: 100%; | ||||
| 				background: #020202; | ||||
| 				display: flex; | ||||
| 				align-items: stretch; | ||||
| 				margin: 0; | ||||
| 				min-width: min-content; | ||||
| 			} | ||||
| 			h3, h4 { | ||||
| 				margin: 10px 0; | ||||
| 			button, | ||||
| 			input, | ||||
| 			.tab { | ||||
| 				padding: 5px 7px; | ||||
| 			} | ||||
| 			button { | ||||
| 				border-color: #767676; | ||||
| 				background: #303030; | ||||
| 			} | ||||
| 			input { | ||||
| 				background: #171717; | ||||
| 				border-bottom: 2px solid transparent; | ||||
| 				padding-bottom: 3px; | ||||
| 			} | ||||
| 			input:focus { | ||||
| 				border-bottom: 2px solid #4f4f4f; | ||||
| 			} | ||||
| 			input::placeholder { | ||||
| 				color: #aaa; | ||||
| 			} | ||||
| 			.thread:hover, | ||||
| 			.tab:hover { | ||||
| 				background-color: #3b3b3b; | ||||
| 			} | ||||
| 			.tab.active, | ||||
| 			.thread.active, | ||||
| 			button:hover { | ||||
| 				background-color: #4f4f4f; | ||||
| 				color: #fff; | ||||
| 			} | ||||
| 			label.heading { | ||||
| 				margin-bottom: 5px; | ||||
| 				display: block; | ||||
| 			} | ||||
| 			h3, | ||||
| 			h4 { | ||||
| 				margin: 10px 0; | ||||
| 			} | ||||
| 			.hidden { | ||||
| 				display: none; | ||||
| 				display: none !important; | ||||
| 			} | ||||
| 			.column { | ||||
| 				flex: 1; | ||||
|  | @ -44,11 +81,10 @@ | |||
| 			#threads { | ||||
| 				max-width: 250px; | ||||
| 			} | ||||
| 			.thread.selected { | ||||
| 				background-color: #4f4f4f; | ||||
| 			} | ||||
| 			.thread:hover { | ||||
| 				background-color: #3b3b3b; | ||||
| 			.thread { | ||||
| 				display: block; | ||||
| 				width: 100%; | ||||
| 				text-align: left; | ||||
| 			} | ||||
| 			#newthread { | ||||
| 				margin-top: 5px; | ||||
|  | @ -59,28 +95,46 @@ | |||
| 			#permissions { | ||||
| 				margin-bottom: 5px; | ||||
| 			} | ||||
| 			#thread { | ||||
| 				display: flex; | ||||
| 				flex-direction: column; | ||||
| 			} | ||||
| 			#title { | ||||
| 				margin: 4px 2px; | ||||
| 			} | ||||
| 			#tabs { | ||||
| 				margin-block: 2px; | ||||
| 			} | ||||
| 			.tab { | ||||
| 				padding: 5px 7px; | ||||
| 				background-color: #1f1f1f; | ||||
| 				border: 0; | ||||
| 				margin-top: 2px; | ||||
| 				padding: 5px 7px; | ||||
| 				color: #ccc; | ||||
| 				color: #ddd; | ||||
| 				font-weight: 500; | ||||
| 			} | ||||
| 			.tab.active { | ||||
| 				background-color: #4f4f4f; | ||||
| 				color: #fff; | ||||
| 			.tabcontent { | ||||
| 				flex-grow: 1; | ||||
| 			} | ||||
| 			.tab:hover { | ||||
| 				background-color:#3b3b3b; | ||||
| 			#message { | ||||
| 				display: flex; | ||||
| 				flex-direction: column; | ||||
| 			} | ||||
| 			#messages { | ||||
| 				margin: 4px 2px; | ||||
| 				flex-grow: 1; | ||||
| 			} | ||||
| 			#msginput { | ||||
| 				margin-top: 15px; | ||||
| 				display: flex; | ||||
| 				flex-direction: row; | ||||
| 			} | ||||
| 			#msg { | ||||
| 				flex-grow: 1; | ||||
| 				margin: 2px; | ||||
| 				padding: 5px; | ||||
| 			} | ||||
| 			#sendmsg { | ||||
| 				margin: 2px 3px; | ||||
| 			} | ||||
| 			.message { | ||||
| 				margin-bottom: 5px; | ||||
|  | @ -97,6 +151,5 @@ | |||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 	</body> | ||||
| 	<body></body> | ||||
| </html> | ||||
|  |  | |||
							
								
								
									
										20
									
								
								index.js
								
								
								
								
							
							
						
						
									
										20
									
								
								index.js
								
								
								
								
							|  | @ -3,15 +3,17 @@ 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()); | ||||
|  |  | |||
|  | @ -85,7 +85,7 @@ const create_thread = async (msg, respond, socket, io) => { | |||
| 	} | ||||
| 	else { | ||||
| 		for (let member of msg.members) { | ||||
| 			for (let socket of io.cache[member.user]) { | ||||
| 			for (let socket of io.cache[member.name]) { | ||||
| 				io.to(socket).emit("new_thread", { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue