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

@ -4,7 +4,7 @@
<script src="/openpgp.min.js"></script> <script src="/openpgp.min.js"></script>
<script src="/socket.io.min.v4.6.1.js"></script> <script src="/socket.io.min.v4.6.1.js"></script>
<script src="/aes.js"></script> <script src="/aes.js"></script>
<script src="/chat.js"></script> <script type="module" src="/ui.js"></script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -28,6 +28,9 @@
min-width: min-content; min-width: min-content;
padding: 0 20px; padding: 0 20px;
} }
#app {
display: contents;
}
.column { .column {
flex: 1; flex: 1;
max-width: 50vw; max-width: 50vw;
@ -58,69 +61,5 @@
</style> </style>
</head> </head>
<body> <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> </body>
</html> </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() { function rand() {
let str = ""; let str = "";
const lookups = const lookups =
@ -16,14 +18,46 @@ async function auth() {
signingKeys: window.keys.priv, signingKeys: window.keys.priv,
}); });
window.session = session; 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 priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub }; 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) { function chooseThread(thread) {
@ -34,9 +68,25 @@ function chooseThread(thread) {
} }
function loadMessages() { function loadMessages() {
window.socket.emit("get_history", { window.emit("get_history", {
before: window.earliestMessage, before: window.earliestMessage,
thread: window.currentThreadId, 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 = ""; 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( const perms = document.querySelector(
'input[name="permissions"]:checked' 'input[name="permissions"]:checked'
).value; ).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 permissions;
let newmembers = members;
if (perms === "public") { if (perms === "public") {
permissions = { permissions = {
view_limited: false, 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; document.getElementById(perms).checked = false;
window.threadmembers = null; window.threadmembers = null;
document.getElementById("memberlist").innerHTML = ""; document.getElementById("memberlist").innerHTML = "";
@ -121,73 +172,11 @@ async function createThread(members) {
member.textContent = window.name; member.textContent = window.name;
member.classList.add("member"); member.classList.add("member");
document.getElementById("memberlist").appendChild(member); document.getElementById("memberlist").appendChild(member);
} window.emit("create_thread", {
name: document.getElementById("newthreadname").value,
function decryptMessage(thread, message) {} permissions,
members,
function encryptMessage(thread, message) {} }, msg => {
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) => {
chooseThread({ chooseThread({
name: document.getElementById("newthreadname").value, name: document.getElementById("newthreadname").value,
id: msg.id, id: msg.id,
@ -196,62 +185,118 @@ window.onload = () => {
document.getElementById("loadmore").classList.add("hidden"); document.getElementById("loadmore").classList.add("hidden");
document.getElementById("msginput").classList.remove("hidden"); document.getElementById("msginput").classList.remove("hidden");
}); });
window.socket.on("get_keys", (msg) => { }
createThread(msg.keys);
}); function sendMessage(e) {
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(); e.preventDefault();
const msg = document.getElementById("msg").value; const msg = document.getElementById("msg").value;
if (!msg) return; if (!msg)
window.socket.emit("send_message", { return;
window.emit("send_message", {
message: msg, message: msg,
thread: window.currentThreadId, thread: window.currentThreadId,
}); });
document.getElementById("msg").value = ""; document.getElementById("msg").value = "";
}; }
document.getElementById("loadmore").onclick = (e) => {
loadMessages(); render(document.body, html`
}; <div id="register" class="hidden">
document.getElementById("createthread").onsubmit = (e) => { <h1>welcome to vybe</h1>
e.preventDefault(); <h3>a communication network (beta)</h3>
const members = window.threadmembers <p>
? window.threadmembers.map((i) => ({ name: i })) to get started, you'll need an account. we use public key cryptography
: [{ name: window.name }]; for security, rather than passwords. your keys are stored in your
const perms = document.querySelector( browser storage only, so do this on a browser you can access again.
'input[name="permissions"]:checked' </p>
).value; <form onsubmit=${register} id="registerform">
console.log(perms, members); <label for="name">username: </label>
if (perms === "private_view") { <input type="text" id="name" />
window.socket.emit("get_keys", { names: members.map((i) => i.name) }); <input id="submit" type="submit" value='generate keys & register'>
} else { </form>
createThread(members); </div>
} <div id="app" class="hidden">
}; <div id="threads" class="column">
document.getElementById("membername").onkeydown = (e) => { <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") { if (e.key == "Enter") {
e.preventDefault();
addMember(); addMember();
} }
}; }}/>
document.getElementById("addmember").onclick = 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>
`);
const keys = localStorage.getItem("keys"); 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) { if (keys) {
window.name = localStorage.getItem("name"); window.name = localStorage.getItem("name");
loadKeys(JSON.parse(keys)).then(() => {}); keys = JSON.parse(keys);
} else document.getElementById("register").classList.remove("hidden"); const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.keys = { priv, pub };
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 sqlite3 = require("sqlite3");
const fs = require("fs"); const fs = require("fs");
const db = new sqlite3.Database("vybe.db"); const db = new sqlite3.Database("vybe.db");
db.query = function (sql, params) { db.query = function (sql, params) {

View File

@ -3,6 +3,16 @@ const http = require("http");
const { Server } = require("socket.io"); const { Server } = require("socket.io");
const compression = require("compression"); 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(); const app = express();
app.use(compression()); app.use(compression());
const server = http.createServer(app); const server = http.createServer(app);
@ -14,19 +24,12 @@ const io = new Server(server, {
const PORT = process.env.PORT || 3435; const PORT = process.env.PORT || 3435;
const actions = require("./src/actions");
io.cache = {}; io.cache = {};
io.on("connection", (socket) => { io.on("connection", (socket) => {
for (let action in actions) { for (let event in events) {
socket.on(action, (msg) => socket.on(event, (msg, callback) =>
actions[action]( events[event](msg, callback, socket, io)
msg,
(response) => socket.emit(action, response),
socket,
io
)
); );
} }
}); });

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 db = require("../db");
const authwrap = (fn) => async (msg, respond, socket, io) => { const authwrap = (fn) => async (msg, respond, socket, io) => {
if (!respond)
respond = () => {};
if (!msg || !msg.__session) { if (!msg || !msg.__session) {
return respond({ return respond({
success: false, success: false,