use socketio callbacks and uhtml

main
jerl 2023-07-30 18:06:21 -07:00
parent 99cb595e07
commit ca78144083
7 changed files with 263 additions and 288 deletions

View File

@ -1,126 +1,65 @@
<!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 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>
<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="/ui.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;
}
#app {
display: contents;
}
.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>
</body>
</html>

2
client/uhtml.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,5 @@
import { render, html } from '/uhtml.js';
function rand() {
let str = "";
const lookups =
@ -16,14 +18,46 @@ async function auth() {
signingKeys: window.keys.priv,
});
window.session = session;
window.socket.emit("authenticate", { name: window.name, message: sig });
window.emit = (type, data, callback) => window.socket.emit(type, {
...data,
__session: window.session,
}, callback);
window.socket.emit("authenticate", { name: window.name, message: sig },
msg => {
if (!msg.success) {
document.getElementById("register").classList.remove("hidden");
return;
}
document.getElementById("register").classList.add("hidden");
document.getElementById("app").classList.remove("hidden");
const member = document.createElement("p");
member.textContent = window.name;
member.classList.add("member");
document.getElementById("memberlist").appendChild(member);
window.emit("list_threads", {}, msg => {
document.getElementById("threadlist").innerHTML = "";
for (let thread of msg.threads)
addThread(thread);
});
loadMessages();
});
}
async function loadKeys(keys) {
async function register(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 };
await auth();
localStorage.setItem("keys", JSON.stringify(keys));
localStorage.setItem("name", name);
window.name = name;
window.emit("create_user", { name, pubkey: keys.publicKey }, auth);
}
function chooseThread(thread) {
@ -34,9 +68,25 @@ function chooseThread(thread) {
}
function loadMessages() {
window.socket.emit("get_history", {
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) {
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");
else
document.getElementById("loadmore").classList.remove("hidden");
});
}
@ -73,12 +123,18 @@ function addMember() {
document.getElementById("membername").value = "";
}
async function createThread(members) {
async function createThread(e) {
e.preventDefault();
let members = window.threadmembers
? window.threadmembers.map((i) => ({ name: i }))
: [{ name: window.name }];
const perms = document.querySelector(
'input[name="permissions"]:checked'
).value;
if (perms === "private_view")
members = (await new Promise(resolve =>
window.emit("get_keys", { names: members.map((i) => i.name) }, resolve))).keys;
let permissions;
let newmembers = members;
if (perms === "public") {
permissions = {
view_limited: false,
@ -109,11 +165,6 @@ async function createThread(members) {
}
*/
}
window.socket.emit("create_thread", {
name: document.getElementById("newthreadname").value,
permissions,
newmembers,
});
document.getElementById(perms).checked = false;
window.threadmembers = null;
document.getElementById("memberlist").innerHTML = "";
@ -121,73 +172,11 @@ async function createThread(members) {
member.textContent = window.name;
member.classList.add("member");
document.getElementById("memberlist").appendChild(member);
}
function decryptMessage(thread, message) {}
function encryptMessage(thread, message) {}
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");
else
document.getElementById("loadmore").classList.remove("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", {});
loadMessages();
} 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) => {
window.emit("create_thread", {
name: document.getElementById("newthreadname").value,
permissions,
members,
}, msg => {
chooseThread({
name: document.getElementById("newthreadname").value,
id: msg.id,
@ -196,62 +185,118 @@ window.onload = () => {
document.getElementById("loadmore").classList.add("hidden");
document.getElementById("msginput").classList.remove("hidden");
});
window.socket.on("get_keys", (msg) => {
createThread(msg.keys);
}
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("registerform").onsubmit = async (e) => {
e.preventDefault();
const name = document.getElementById("name").value;
if (!name) return;
const keys = await openpgp.generateKey({
userIDs: [{ name }],
});
document.getElementById("msg").value = "";
}
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=${register} id="registerform">
<label for="name">username: </label>
<input type="text" id="name" />
<input id="submit" type="submit" value='generate keys & register'>
</form>
</div>
<div id="app" class="hidden">
<div id="threads" class="column">
<h1>vybe</h1>
<h3>threads</h3>
<div id="threadlist">loading...</div>
<form id="createthread" onsubmit=${createThread}>
<h3>create thread</h3>
<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" onkeydown=${(e) => {
if (e.key == "Enter") {
e.preventDefault();
addMember();
}
}}/>
<button id="addmember" onclick=${addMember}>add</button>
<div id="memberlist"></div>
<br />
<input id="submitthread" type="submit" value="create" />
</form>
</div>
<div id="chat" class="column">
<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" onclick=${loadMessages}>load more messages</button>
<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>
`);
window.onload = async () => {
window.currentThreadId = 1;
window.socket = io();
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("new_thread", addThread);
let keys = localStorage.getItem("keys");
if (keys) {
window.name = localStorage.getItem("name");
keys = JSON.parse(keys);
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 members = window.threadmembers
? window.threadmembers.map((i) => ({ name: i }))
: [{ name: window.name }];
const perms = document.querySelector(
'input[name="permissions"]:checked'
).value;
console.log(perms, members);
if (perms === "private_view") {
window.socket.emit("get_keys", { names: members.map((i) => i.name) });
} else {
createThread(members);
}
};
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");
await auth();
document.getElementById('app').classList.remove('hidden');
}
else
document.getElementById('register').classList.remove('hidden');
};

1
db.js
View File

@ -1,5 +1,6 @@
const sqlite3 = require("sqlite3");
const fs = require("fs");
const db = new sqlite3.Database("vybe.db");
db.query = function (sql, params) {

View File

@ -3,36 +3,39 @@ 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 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;
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
)
);
}
for (let event in events) {
socket.on(event, (msg, callback) =>
events[event](msg, callback, socket, io)
);
}
});
server.listen(PORT, () => {
console.log("server running on port " + PORT);
console.log("server running on port " + PORT);
});
app.use(express.static("client"));

View File

@ -1,17 +0,0 @@
const create_user = require("./create_user");
const get_history = require("./get_history");
const send_message = require("./send_message");
const authenticate = require("./authenticate");
const create_thread = require("./create_thread");
const list_threads = require("./list_threads");
const get_keys = require("./get_keys");
module.exports = {
create_user,
get_history,
send_message,
authenticate,
create_thread,
list_threads,
get_keys,
};

View File

@ -1,6 +1,8 @@
const db = require("../db");
const authwrap = (fn) => async (msg, respond, socket, io) => {
if (!respond)
respond = () => {};
if (!msg || !msg.__session) {
return respond({
success: false,