permissions (basic)
							parent
							
								
									85d5297c94
								
							
						
					
					
						commit
						89f8032034
					
				
							
								
								
									
										7
									
								
								DOCS.md
								
								
								
								
							
							
						
						
									
										7
									
								
								DOCS.md
								
								
								
								
							|  | @ -90,7 +90,12 @@ Message format: | |||
| 
 | ||||
| ```json | ||||
| { | ||||
|   "name": "thread name" | ||||
|   "name": "thread name", | ||||
|   "permissions": { | ||||
|     "view_limited": true, | ||||
|     "post_limited": true | ||||
|   }, | ||||
|   "members": ["username1", "username2"] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										335
									
								
								client/chat.js
								
								
								
								
							
							
						
						
									
										335
									
								
								client/chat.js
								
								
								
								
							|  | @ -1,161 +1,218 @@ | |||
| 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() { | ||||
| 	let session = rand(); | ||||
| 	const sig = await openpgp.sign({ | ||||
| 		message: new openpgp.CleartextMessage("vybe_auth " + session, ""), | ||||
| 		signingKeys: window.keys.priv, | ||||
| 	}); | ||||
| 	window.session = session; | ||||
| 	window.socket.emit("authenticate", { name: window.name, message: sig }); | ||||
|   let session = rand(); | ||||
|   const sig = await openpgp.sign({ | ||||
|     message: new openpgp.CleartextMessage("vybe_auth " + session, ""), | ||||
|     signingKeys: window.keys.priv, | ||||
|   }); | ||||
|   window.session = session; | ||||
|   window.socket.emit("authenticate", { name: window.name, message: sig }); | ||||
| } | ||||
| 
 | ||||
| async function loadKeys(keys) { | ||||
| 	const priv = await openpgp.readKey({ armoredKey: keys.privateKey }); | ||||
| 	const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); | ||||
| 	window.keys = { priv, pub }; | ||||
| 	await auth(); | ||||
|   const priv = await openpgp.readKey({ armoredKey: keys.privateKey }); | ||||
|   const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); | ||||
|   window.keys = { priv, pub }; | ||||
|   await auth(); | ||||
| } | ||||
| 
 | ||||
| function chooseThread(thread) { | ||||
| 	window.currentThreadId = thread.id; | ||||
| 	window.earliestMessage = null; | ||||
| 	document.getElementById("messages").innerHTML = ""; | ||||
| 	document.getElementById("threadname").innerHTML = thread.name; | ||||
|   window.currentThreadId = thread.id; | ||||
|   window.earliestMessage = null; | ||||
|   document.getElementById("messages").innerHTML = ""; | ||||
|   document.getElementById("threadname").innerHTML = thread.name; | ||||
| } | ||||
| 
 | ||||
| function loadMessages() { | ||||
| 	window.socket.emit( | ||||
| 		"get_history", | ||||
| 		{ before: window.earliestMessage, thread: window.currentThreadId } | ||||
| 	); | ||||
|   window.socket.emit("get_history", { | ||||
|     before: window.earliestMessage, | ||||
|     thread: window.currentThreadId, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function addThread(thread) { | ||||
| 	const el = document.createElement("div"); | ||||
| 	el.classList.add("thread"); | ||||
| 	el.innerHTML = thread.name; | ||||
| 	const btn = document.createElement("button"); | ||||
| 	btn.innerHTML = "choose"; | ||||
| 	btn.onclick = () => { | ||||
| 		chooseThread(thread); | ||||
| 		loadMessages(); | ||||
| 		document.getElementById("loadmore").classList.remove("hidden"); | ||||
| 	}; | ||||
| 	el.appendChild(btn); | ||||
| 	document.getElementById("threadlist").appendChild(el); | ||||
|   const el = document.createElement("div"); | ||||
|   el.classList.add("thread"); | ||||
|   el.innerHTML = thread.name; | ||||
|   const btn = document.createElement("button"); | ||||
|   btn.innerHTML = "choose"; | ||||
|   btn.onclick = () => { | ||||
|     chooseThread(thread); | ||||
|     loadMessages(); | ||||
|     document.getElementById("loadmore").classList.remove("hidden"); | ||||
|     if (!thread.permissions.post) { | ||||
|       document.getElementById("msginput").classList.add("hidden"); | ||||
|     } else { | ||||
|       document.getElementById("msginput").classList.remove("hidden"); | ||||
|     } | ||||
|   }; | ||||
|   el.appendChild(btn); | ||||
|   document.getElementById("threadlist").appendChild(el); | ||||
| } | ||||
| 
 | ||||
| function addMember() { | ||||
|   const name = document.getElementById("membername").value; | ||||
|   if (!window.threadmembers) { | ||||
|     window.threadmembers = [window.name, name]; | ||||
|   } else { | ||||
|     window.threadmembers.push(name); | ||||
|   } | ||||
|   const member = document.createElement("p"); | ||||
|   member.textContent = name; | ||||
|   member.classList.add("member"); | ||||
|   document.getElementById("memberlist").appendChild(member); | ||||
|   document.getElementById("membername").value = ""; | ||||
| } | ||||
| 
 | ||||
| window.onload = () => { | ||||
| 	window.currentThreadId = 1; | ||||
| 	window.socket = io(); | ||||
| 	window.socket.on("create_user", auth); | ||||
| 	window.socket.on("new_message", (msg) => { | ||||
| 		if (msg.thread !== window.currentThreadId) return; | ||||
| 		const el = document.createElement("div"); | ||||
| 		el.classList.add("message"); | ||||
| 		const strong = document.createElement('strong'); | ||||
| 		strong.textContent = msg.name + ': '; | ||||
| 		el.append(strong, msg.message); | ||||
| 		document.getElementById("messages").appendChild(el); | ||||
| 		if (!window.earliestMessage) | ||||
| 			window.earliestMessage = msg.id; | ||||
| 	}); | ||||
| 	window.socket.on("get_history", (msg) => { | ||||
| 		if (msg.messages.length > 0) { | ||||
| 			window.earliestMessage = msg.messages[msg.messages.length - 1].id; | ||||
| 			for (let message of msg.messages) { | ||||
| 				const el = document.createElement("div"); | ||||
| 				el.classList.add("message"); | ||||
| 				const strong = document.createElement('strong'); | ||||
| 				strong.textContent = message.name + ': '; | ||||
| 				el.append(strong, message.message); | ||||
| 				document.getElementById("messages").prepend(el); | ||||
| 			} | ||||
| 		} | ||||
| 		if (!msg.more) | ||||
| 			document.getElementById("loadmore").classList.add("hidden"); | ||||
| 	}); | ||||
| 	window.socket.on("authenticate", (msg) => { | ||||
| 		if (msg.success) { | ||||
| 			document.getElementById("register").classList.add("hidden"); | ||||
| 			document.getElementById("threads").classList.remove("hidden"); | ||||
| 			document.getElementById("chat").classList.remove("hidden"); | ||||
| 			window.socket.emit("list_threads"); | ||||
| 		} | ||||
| 		let emitter = window.socket.emit; | ||||
| 		window.socket.emit = (type, data) => { | ||||
| 			if (data) | ||||
| 				return emitter.call(window.socket, type, { | ||||
| 					...data, | ||||
| 					__session: window.session, | ||||
| 				}); | ||||
| 			else return emitter.call(window.socket, type); | ||||
| 		}; | ||||
| 	}); | ||||
| 	window.socket.on("list_threads", (msg) => { | ||||
| 		document.getElementById("threadlist").innerHTML = ""; | ||||
| 		for (let thread of msg.threads) | ||||
| 			addThread(thread); | ||||
| 	}); | ||||
| 	window.socket.on('new_thread', addThread); | ||||
| 	window.socket.on("create_thread", (msg) => { | ||||
| 		chooseThread({ | ||||
| 			name: document.getElementById("newthreadname").value, | ||||
| 			id: msg.id, | ||||
| 		}); | ||||
| 		document.getElementById("newthreadname").value = ""; | ||||
| 	}); | ||||
|   window.currentThreadId = 1; | ||||
|   window.socket = io(); | ||||
|   window.socket.on("create_user", auth); | ||||
|   window.socket.on("new_message", (msg) => { | ||||
|     if (msg.thread !== window.currentThreadId) return; | ||||
|     const el = document.createElement("div"); | ||||
|     el.classList.add("message"); | ||||
|     const strong = document.createElement("strong"); | ||||
|     strong.textContent = msg.name + ": "; | ||||
|     el.append(strong, msg.message); | ||||
|     document.getElementById("messages").appendChild(el); | ||||
|     if (!window.earliestMessage) window.earliestMessage = msg.id; | ||||
|   }); | ||||
|   window.socket.on("get_history", (msg) => { | ||||
|     if (msg.messages.length > 0) { | ||||
|       window.earliestMessage = msg.messages[msg.messages.length - 1].id; | ||||
|       for (let message of msg.messages) { | ||||
|         const el = document.createElement("div"); | ||||
|         el.classList.add("message"); | ||||
|         const strong = document.createElement("strong"); | ||||
|         strong.textContent = message.name + ": "; | ||||
|         el.append(strong, message.message); | ||||
|         document.getElementById("messages").prepend(el); | ||||
|       } | ||||
|     } | ||||
|     if (!msg.more) document.getElementById("loadmore").classList.add("hidden"); | ||||
|   }); | ||||
|   window.socket.on("authenticate", (msg) => { | ||||
|     if (msg.success) { | ||||
|       document.getElementById("register").classList.add("hidden"); | ||||
|       document.getElementById("threads").classList.remove("hidden"); | ||||
|       document.getElementById("chat").classList.remove("hidden"); | ||||
|       const member = document.createElement("p"); | ||||
|       member.textContent = window.name; | ||||
|       member.classList.add("member"); | ||||
|       document.getElementById("memberlist").appendChild(member); | ||||
|       let emitter = window.socket.emit; | ||||
|       window.socket.emit = (type, data) => { | ||||
|         if (data) | ||||
|           return emitter.call(window.socket, type, { | ||||
|             ...data, | ||||
|             __session: window.session, | ||||
|           }); | ||||
|         else return emitter.call(window.socket, type); | ||||
|       }; | ||||
|       window.socket.emit("list_threads", {}); | ||||
|     } else { | ||||
|       document.getElementById("register").classList.remove("hidden"); | ||||
|     } | ||||
|   }); | ||||
|   window.socket.on("list_threads", (msg) => { | ||||
|     document.getElementById("threadlist").innerHTML = ""; | ||||
|     for (let thread of msg.threads) addThread(thread); | ||||
|   }); | ||||
|   window.socket.on("new_thread", addThread); | ||||
|   window.socket.on("create_thread", (msg) => { | ||||
|     chooseThread({ | ||||
|       name: document.getElementById("newthreadname").value, | ||||
|       id: msg.id, | ||||
|     }); | ||||
|     document.getElementById("newthreadname").value = ""; | ||||
|     document.getElementById("loadmore").classList.add("hidden"); | ||||
|     document.getElementById("msginput").classList.remove("hidden"); | ||||
|   }); | ||||
| 
 | ||||
| 	document.getElementById("registerform").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.keys = { priv, pub }; | ||||
| 		localStorage.setItem("keys", JSON.stringify(keys)); | ||||
| 		localStorage.setItem("name", name); | ||||
| 		window.name = name; | ||||
| 		window.socket.emit("create_user", { name, pubkey: keys.publicKey }); | ||||
| 	}; | ||||
| 	document.getElementById("msginput").onsubmit = e => { | ||||
| 		e.preventDefault(); | ||||
| 		const msg = document.getElementById("msg").value; | ||||
| 		if (!msg) return; | ||||
| 		window.socket.emit("send_message", { | ||||
| 			message: msg, | ||||
| 			thread: window.currentThreadId, | ||||
| 		}); | ||||
| 		document.getElementById("msg").value = ""; | ||||
| 	}; | ||||
| 	document.getElementById("loadmore").onclick = e => { | ||||
| 		loadMessages(); | ||||
| 	}; | ||||
| 	document.getElementById("createthread").onsubmit = e => { | ||||
| 		e.preventDefault(); | ||||
| 		window.socket.emit("create_thread", { | ||||
| 			name: document.getElementById("newthreadname").value, | ||||
| 		}); | ||||
| 	}; | ||||
|   document.getElementById("registerform").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.keys = { priv, pub }; | ||||
|     localStorage.setItem("keys", JSON.stringify(keys)); | ||||
|     localStorage.setItem("name", name); | ||||
|     window.name = name; | ||||
|     window.socket.emit("create_user", { name, pubkey: keys.publicKey }); | ||||
|   }; | ||||
|   document.getElementById("msginput").onsubmit = (e) => { | ||||
|     e.preventDefault(); | ||||
|     const msg = document.getElementById("msg").value; | ||||
|     if (!msg) return; | ||||
|     window.socket.emit("send_message", { | ||||
|       message: msg, | ||||
|       thread: window.currentThreadId, | ||||
|     }); | ||||
|     document.getElementById("msg").value = ""; | ||||
|   }; | ||||
|   document.getElementById("loadmore").onclick = (e) => { | ||||
|     loadMessages(); | ||||
|   }; | ||||
|   document.getElementById("createthread").onsubmit = (e) => { | ||||
|     e.preventDefault(); | ||||
|     const perms = document.querySelector( | ||||
|       'input[name="permissions"]:checked' | ||||
|     ).value; | ||||
|     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, | ||||
|       }; | ||||
|     } | ||||
|     window.socket.emit("create_thread", { | ||||
|       name: document.getElementById("newthreadname").value, | ||||
|       permissions, | ||||
|       members: window.threadmembers || [window.name], | ||||
|     }); | ||||
|     document.getElementById(perms).checked = false; | ||||
|     window.threadmembers = null; | ||||
|     document.getElementById("memberlist").innerHTML = ""; | ||||
|     const member = document.createElement("p"); | ||||
|     member.textContent = window.name; | ||||
|     member.classList.add("member"); | ||||
|     document.getElementById("memberlist").appendChild(member); | ||||
|   }; | ||||
|   document.getElementById("membername").onkeydown = (e) => { | ||||
|     if (e.key == "Enter") { | ||||
|       addMember(); | ||||
|     } | ||||
|   }; | ||||
|   document.getElementById("addmember").onclick = addMember; | ||||
| 
 | ||||
| 	const keys = localStorage.getItem("keys"); | ||||
| 	if (keys) { | ||||
| 		window.name = localStorage.getItem("name"); | ||||
| 		loadKeys(JSON.parse(keys)).then(() => {}); | ||||
| 	} | ||||
| 	else | ||||
| 		document.getElementById("register").classList.remove("hidden"); | ||||
|   const keys = localStorage.getItem("keys"); | ||||
|   if (keys) { | ||||
|     window.name = localStorage.getItem("name"); | ||||
|     loadKeys(JSON.parse(keys)).then(() => {}); | ||||
|   } else document.getElementById("register").classList.remove("hidden"); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,89 +1,125 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| 	<head> | ||||
| 		<script src="/openpgp.min.js"></script> | ||||
| 		<script src="/socket.io.min.v4.6.1.js"></script> | ||||
| 		<script src="/chat.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; | ||||
| 			} | ||||
| 			.column { | ||||
| 				flex-grow: 1; | ||||
| 			} | ||||
| 			button { | ||||
| 				border-color: #767676; | ||||
| 			} | ||||
| 			.hidden { | ||||
| 				display: none; | ||||
| 			} | ||||
| 			#msginput { | ||||
| 				margin-top: 15px; | ||||
| 			} | ||||
| 			.message { | ||||
| 				margin-bottom: 5px; | ||||
| 			} | ||||
| 			#loadmore { | ||||
| 				margin-bottom: 10px; | ||||
| 			} | ||||
| 			.channel { | ||||
| 				font-weight: normal; | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<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. for now | ||||
| 				your keys are stored in your browser storage only. | ||||
| 			</p> | ||||
| 			<form id="registerform"> | ||||
| 				<label for="name">name/username: </label> | ||||
| 				<input type="text" id="name" /> | ||||
| 				<button id="submit" type="submit">generate keys & register</button> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 		<div id="threads" class="column hidden"> | ||||
| 			<h1>vybe</h1> | ||||
| 			<h3>threads</h3> | ||||
| 			<div id="threadlist">loading...</div> | ||||
| 			<h3>create thread</h3> | ||||
| 			<form id="createthread"> | ||||
| 				<label for="newthreadname">thread name</label> | ||||
| 				<input type="text" id="newthreadname" /> | ||||
| 				<button id="submitthread" type="submit">create</button> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 		<div id="chat" class="column hidden"> | ||||
| 			<h3 class="thread"> | ||||
| 				current thread: <strong id="threadname">meow</strong> | ||||
| 			</h3> | ||||
| 			<h3>messages will appear below as they are sent</h3> | ||||
| 			<button id="loadmore">load more messages</button> | ||||
| 			<div id="messages"></div> | ||||
| 			<form id="msginput"> | ||||
| 				<input type="text" placeholder="write a message..." id="msg" /> | ||||
| 				<button type="submit" class="hidden" id="sendmsg"></button> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	</body> | ||||
|   <head> | ||||
|     <script src="/openpgp.min.js"></script> | ||||
|     <script src="/socket.io.min.v4.6.1.js"></script> | ||||
|     <script src="/chat.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; | ||||
|         padding: 0 20px; | ||||
|       } | ||||
|       .column { | ||||
|         flex: 1; | ||||
|         max-width: 50vw; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|       button { | ||||
|         border-color: #767676; | ||||
|       } | ||||
|       .hidden { | ||||
|         display: none; | ||||
|       } | ||||
|       #msginput { | ||||
|         margin-top: 15px; | ||||
|       } | ||||
|       .message { | ||||
|         margin-bottom: 5px; | ||||
|         overflow-wrap: break-word; | ||||
|       } | ||||
|       #loadmore { | ||||
|         margin-bottom: 10px; | ||||
|       } | ||||
|       .channel { | ||||
|         font-weight: normal; | ||||
|       } | ||||
|       .member { | ||||
|         margin: 5px 0; | ||||
|       } | ||||
|     </style> | ||||
|   </head> | ||||
|   <body> | ||||
|     <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. for now your keys are stored in | ||||
|         your browser storage only. | ||||
|       </p> | ||||
|       <form id="registerform"> | ||||
|         <label for="name">name/username: </label> | ||||
|         <input type="text" id="name" /> | ||||
|         <button id="submit" type="submit">generate keys & register</button> | ||||
|       </form> | ||||
|     </div> | ||||
|     <div id="threads" class="column hidden"> | ||||
|       <h1>vybe</h1> | ||||
|       <h3>threads</h3> | ||||
|       <div id="threadlist">loading...</div> | ||||
|       <h3>create thread</h3> | ||||
|       <form id="createthread"> | ||||
|         <label for="newthreadname">thread name</label> | ||||
|         <input type="text" id="newthreadname" /><br /><br /> | ||||
|         <span>thread permissions</span><br /> | ||||
|         <input type="radio" id="public" name="permissions" value="public" /> | ||||
|         <label for="public">anyone can view and post</label><br /> | ||||
|         <input | ||||
|           type="radio" | ||||
|           id="private_post" | ||||
|           name="permissions" | ||||
|           value="private_post" | ||||
|         /> | ||||
|         <label for="private_post">only members can post, anyone can view</label | ||||
|         ><br /> | ||||
|         <input | ||||
|           type="radio" | ||||
|           id="private_view" | ||||
|           name="permissions" | ||||
|           value="private_view" | ||||
|         /> | ||||
|         <label for="private_view">only members can view and post</label | ||||
|         ><br /><br /> | ||||
|         <span>members</span><br /> | ||||
|         <input type="text" id="membername" placeholder="username" /><button | ||||
|           id="addmember" | ||||
|         > | ||||
|           add | ||||
|         </button> | ||||
|         <div id="memberlist"></div> | ||||
|         <br /> | ||||
|         <button id="submitthread" type="submit">create</button> | ||||
|       </form> | ||||
|     </div> | ||||
|     <div id="chat" class="column hidden"> | ||||
|       <h3 class="thread"> | ||||
|         current thread: <strong id="threadname">meow</strong> | ||||
|       </h3> | ||||
|       <h3>messages will appear below as they are sent</h3> | ||||
|       <button id="loadmore" class="hidden">load more messages</button> | ||||
|       <div id="messages"></div> | ||||
|       <form id="msginput"> | ||||
|         <input type="text" placeholder="write a message..." id="msg" /> | ||||
|         <button type="submit" class="hidden" id="sendmsg"></button> | ||||
|       </form> | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -28,14 +28,14 @@ CREATE TABLE permissions ( | |||
|   value text, | ||||
|   foreign key(user) references users(id), | ||||
|   foreign key(thread) references threads(id) | ||||
| ) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE members ( | ||||
|   thread integer, | ||||
|   user integer, | ||||
|   foreign key(user) references users(id), | ||||
|   foreign key(thread) references threads(id) | ||||
| ) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE posts ( | ||||
|   id integer primary key asc, | ||||
|  |  | |||
|  | @ -9,8 +9,8 @@ | |||
| ## permissions -> permission | ||||
| 
 | ||||
| - manage_permissions | ||||
| - add_users | ||||
| - remove_users | ||||
| - add_members | ||||
| - remove_members | ||||
| - view | ||||
| - post | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										9
									
								
								index.js
								
								
								
								
							
							
						
						
									
										9
									
								
								index.js
								
								
								
								
							|  | @ -14,10 +14,17 @@ const PORT = process.env.PORT || 3435; | |||
| 
 | ||||
| const actions = require("./src/actions"); | ||||
| 
 | ||||
| io.cache = {}; | ||||
| 
 | ||||
| io.on("connection", (socket) => { | ||||
|   for (let action in actions) { | ||||
|     socket.on(action, (msg) => | ||||
|       actions[action](msg, (response) => socket.emit(action, response), socket, io) | ||||
|       actions[action]( | ||||
|         msg, | ||||
|         (response) => socket.emit(action, response), | ||||
|         socket, | ||||
|         io | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
| }); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| const db = require("../db"); | ||||
| const openpgp = require("openpgp"); | ||||
| 
 | ||||
| const authenticate = async (msg, respond, socket) => { | ||||
| const authenticate = async (msg, respond, socket, io) => { | ||||
|   if (!msg.name || !msg.message) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|  | @ -43,6 +43,11 @@ const authenticate = async (msg, respond, socket) => { | |||
|         data[1], | ||||
|       ]); | ||||
|       socket.userid = result.rows[0].id; | ||||
|       if (io.cache[msg.name]) { | ||||
|         io.cache[msg.name].push(socket.id); | ||||
|       } else { | ||||
|         io.cache[msg.name] = [socket.id]; | ||||
|       } | ||||
|       return respond({ | ||||
|         success: true, | ||||
|       }); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| const db = require("../db"); | ||||
| 
 | ||||
| const authwrap = (fn) => async (msg, respond, socket, io) => { | ||||
|   if (!msg.__session) { | ||||
|   if (!msg || !msg.__session) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "Not authenticated", | ||||
|  |  | |||
|  | @ -20,10 +20,82 @@ const create_thread = async (msg, respond, socket, io) => { | |||
|     "insert into threads (name, creator) values (?, ?) returning id", | ||||
|     [msg.name, msg.auth_user.id] | ||||
|   ); | ||||
|   io.emit('new_thread', { | ||||
|     name: msg.name, | ||||
|     id: insert.rows[0].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, flexible, permission, value) 
 | ||||
|       values (?, ?, ?, ?, ?)`,
 | ||||
|       [thread_id, "everyone", false, "view", "true"] | ||||
|     ); | ||||
|     if (!msg.permissions || !msg.permissions.post_limited) { | ||||
|       await db.query( | ||||
|         `insert into permissions (thread, type, flexible, permission, value) 
 | ||||
|         values (?, ?, ?, ?, ?)`,
 | ||||
|         [thread_id, "everyone", false, "post", "true"] | ||||
|       ); | ||||
|     } else { | ||||
|       await db.query( | ||||
|         `insert into permissions (thread, type, flexible, permission, value) 
 | ||||
|         values (?, ?, ?, ?, ?)`,
 | ||||
|         [thread_id, "members", false, "post", "true"] | ||||
|       ); | ||||
|     } | ||||
|   } else { | ||||
|     await db.query( | ||||
|       `insert into permissions (thread, type, flexible, permission, value) 
 | ||||
|       values (?, ?, ?, ?, ?)`,
 | ||||
|       [thread_id, "members", false, "view", "true"] | ||||
|     ); | ||||
|     await db.query( | ||||
|       `insert into permissions (thread, type, flexible, permission, value) 
 | ||||
|       values (?, ?, ?, ?, ?)`,
 | ||||
|       [thread_id, "members", false, "post", "true"] | ||||
|     ); | ||||
|   } | ||||
|   // add members
 | ||||
|   for (let user of msg.members) { | ||||
|     // get user id
 | ||||
|     const id = await db.query("select id from users where name = ?", [user]); | ||||
|     if (id.rows.length > 0) { | ||||
|       const user_id = id.rows[0].id; | ||||
|       await db.query("insert into members (thread, user) values (?, ?)", [ | ||||
|         thread_id, | ||||
|         user_id, | ||||
|       ]); | ||||
|     } | ||||
|   } | ||||
|   const member_perms = { | ||||
|     is_member: true, | ||||
|     view: true, | ||||
|     post: true, | ||||
|   }; | ||||
|   const general_perms = { | ||||
|     is_member: false, | ||||
|     view: !msg.permissions || !msg.permissions.view_limited, | ||||
|     post: !msg.permissions || !msg.permissions.post_limited, | ||||
|   }; | ||||
|   for (let username in io.cache) { | ||||
|     if (msg.members.includes(username)) { | ||||
|       const sockets = io.cache[username]; | ||||
|       for (let s of sockets) { | ||||
|         io.to(s).emit("new_thread", { | ||||
|           name: msg.name, | ||||
|           id: insert.rows[0].id, | ||||
|           permissions: member_perms, | ||||
|         }); | ||||
|       } | ||||
|     } else if (general_perms.view) { | ||||
|       const sockets = io.cache[username]; | ||||
|       for (let s of sockets) { | ||||
|         io.to(s).emit("new_thread", { | ||||
|           name: msg.name, | ||||
|           id: insert.rows[0].id, | ||||
|           permissions: general_perms, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   // respond
 | ||||
|   return respond({ | ||||
|     success: true, | ||||
|  |  | |||
|  | @ -1,4 +1,6 @@ | |||
| const db = require("../db"); | ||||
| const authwrap = require("./authwrap"); | ||||
| const check_permissions = require("./helpers/check_permissions"); | ||||
| 
 | ||||
| const get_history = async (msg, respond) => { | ||||
|   if (msg.before && isNaN(Number(msg.before))) { | ||||
|  | @ -13,6 +15,12 @@ const get_history = async (msg, respond) => { | |||
|       message: "thread ID required", | ||||
|     }); | ||||
|   } | ||||
|   if (!(await check_permissions(msg.auth_user.id, msg.thread)).view) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "you can't view this thread", | ||||
|     }); | ||||
|   } | ||||
|   const messages = await db.query( | ||||
|     `select users.name, posts.id, content from posts
 | ||||
|     join users on posts.user = users.id | ||||
|  | @ -31,4 +39,4 @@ const get_history = async (msg, respond) => { | |||
|   }); | ||||
| }; | ||||
| 
 | ||||
| module.exports = get_history; | ||||
| module.exports = authwrap(get_history); | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| const db = require("../../db"); | ||||
| 
 | ||||
| const check_permissions = async (user_id, thread_id) => { | ||||
|   // get all the permissions for the thread
 | ||||
|   const permissions = await db.query( | ||||
|     "select * from permissions where thread = ?", | ||||
|     [thread_id] | ||||
|   ); | ||||
|   // check if the user is a member
 | ||||
|   const is_member = | ||||
|     ( | ||||
|       await db.query("select * from members where thread = ? and user = ?", [ | ||||
|         thread_id, | ||||
|         user_id, | ||||
|       ]) | ||||
|     ).rows.length > 0; | ||||
|   const get_permission = (permission) => { | ||||
|     const relevant = permissions.rows.filter( | ||||
|       (i) => i.permission === permission | ||||
|     ); | ||||
|     for (let i of relevant) { | ||||
|       if (i.type === "everyone" && i.value === "true") { | ||||
|         return true; | ||||
|       } | ||||
|       if (i.type === "members" && i.value === "true" && is_member) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|   return { | ||||
|     is_member, | ||||
|     view: get_permission("view"), | ||||
|     post: get_permission("post"), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| module.exports = check_permissions; | ||||
|  | @ -1,14 +1,32 @@ | |||
| const db = require("../db"); | ||||
| const authwrap = require("./authwrap"); | ||||
| const check_permissions = require("./helpers/check_permissions"); | ||||
| 
 | ||||
| const list_threads = async (msg, respond) => { | ||||
|   const threads = await db.query( | ||||
|     "select name, id from threads order by created desc" | ||||
|     `select name, id from threads
 | ||||
|     join permissions on threads.id = permissions.thread | ||||
|     join members on threads.id = members.thread | ||||
|     where permissions.permission = 'view' | ||||
|     and permissions.value = 'true' | ||||
|     and ((permissions.type = 'everyone') or | ||||
|       permissions.type = 'members' and members.user = ?) | ||||
|     group by threads.id | ||||
|     order by created desc`,
 | ||||
|     [msg.auth_user.id] | ||||
|   ); | ||||
|   // respond
 | ||||
|   const rows = []; | ||||
|   for (let i of threads.rows) { | ||||
|     rows.push({ | ||||
|       ...i, | ||||
|       permissions: await check_permissions(msg.auth_user.id, i.id), | ||||
|     }); | ||||
|   } | ||||
|   return respond({ | ||||
|     success: true, | ||||
|     threads: threads.rows, | ||||
|     threads: rows, | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| module.exports = list_threads; | ||||
| module.exports = authwrap(list_threads); | ||||
|  |  | |||
|  | @ -1,24 +1,60 @@ | |||
| const db = require("../db"); | ||||
| const authwrap = require("./authwrap"); | ||||
| const check_permissions = require("./helpers/check_permissions"); | ||||
| 
 | ||||
| const send_message = async (msg, respond, socket) => { | ||||
| const send_message = async (msg, respond, socket, io) => { | ||||
|   if (!msg.thread) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "thread ID required", | ||||
|     }); | ||||
|   } | ||||
|   if (!(await check_permissions(msg.auth_user.id, msg.thread)).post) { | ||||
|     return respond({ | ||||
|       success: false, | ||||
|       message: "you can't post to this thread", | ||||
|     }); | ||||
|   } | ||||
|   // add message and send it to everyone
 | ||||
|   const id = await db.query( | ||||
|     "insert into posts (user, thread, content) values (?, ?, ?) returning id", | ||||
|     [msg.auth_user.id, msg.thread, msg.message] | ||||
|   ); | ||||
|   socket.broadcast.emit("new_message", { | ||||
|     id: id.rows[0].id, | ||||
|     name: msg.auth_user.name, | ||||
|     message: msg.message, | ||||
|     thread: msg.thread, | ||||
|   }); | ||||
|   // get thread members
 | ||||
|   const members = ( | ||||
|     await db.query( | ||||
|       "select name from users join members on members.user = users.id where members.thread = ?", | ||||
|       [msg.thread] | ||||
|     ) | ||||
|   ).rows.map((i) => i.name); | ||||
|   // get perms
 | ||||
|   const permissions = await db.query( | ||||
|     "select * from permissions where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'", | ||||
|     [msg.thread] | ||||
|   ); | ||||
|   for (let username in io.cache) { | ||||
|     if (members.includes(username)) { | ||||
|       const sockets = io.cache[username]; | ||||
|       for (let s of sockets) { | ||||
|         io.to(s).emit("new_message", { | ||||
|           id: id.rows[0].id, | ||||
|           name: msg.auth_user.name, | ||||
|           message: msg.message, | ||||
|           thread: msg.thread, | ||||
|         }); | ||||
|       } | ||||
|     } else if (permissions.rows.length > 0) { | ||||
|       const sockets = io.cache[username]; | ||||
|       for (let s of sockets) { | ||||
|         io.to(s).emit("new_message", { | ||||
|           id: id.rows[0].id, | ||||
|           name: msg.auth_user.name, | ||||
|           message: msg.message, | ||||
|           thread: msg.thread, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return respond({ | ||||
|     success: true, | ||||
|     id: id.rows[0].id, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue