306 lines
8.8 KiB
JavaScript
306 lines
8.8 KiB
JavaScript
import { render, html } from '/uhtml.js';
|
|
|
|
function rand() {
|
|
let str = "";
|
|
const lookups =
|
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split("");
|
|
while (str.length < 16) {
|
|
const n = Math.random() * lookups.length;
|
|
str += lookups[Math.floor(n)];
|
|
}
|
|
return str;
|
|
}
|
|
|
|
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.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");
|
|
window.emit("list_threads", {}, msg => {
|
|
document.getElementById("threadlist").innerHTML = "";
|
|
for (let thread of msg.threads)
|
|
addThread(thread);
|
|
});
|
|
loadMessages();
|
|
});
|
|
}
|
|
|
|
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 };
|
|
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) {
|
|
window.currentThreadId = thread.id;
|
|
window.earliestMessage = null;
|
|
document.getElementById("messages").innerHTML = "";
|
|
document.getElementById("threadname").innerHTML = thread.name;
|
|
}
|
|
|
|
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) {
|
|
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");
|
|
});
|
|
}
|
|
|
|
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();
|
|
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);
|
|
}
|
|
document.getElementById("memberlist").appendChild(html.node`
|
|
<p class='member'>${name}</p>
|
|
`);
|
|
document.getElementById("membername").value = "";
|
|
}
|
|
|
|
async function createThread(e) {
|
|
e.preventDefault();
|
|
let members = window.threadmembers.map(name => { name });
|
|
const perms = document.querySelector(
|
|
'input[name="permissions"]:checked'
|
|
).value;
|
|
if (perms === "private_view")
|
|
members = (await new Promise(resolve =>
|
|
window.emit("get_keys", { names: window.threadmembers }, resolve)
|
|
)).keys;
|
|
let permissions;
|
|
if (perms === "public") {
|
|
permissions = {
|
|
view_limited: false,
|
|
post_limited: false,
|
|
};
|
|
} else if (perms === "private_post") {
|
|
permissions = {
|
|
view_limited: false,
|
|
post_limited: true,
|
|
};
|
|
} else if (perms === "private_view") {
|
|
permissions = {
|
|
view_limited: true,
|
|
post_limited: true,
|
|
};
|
|
// generate key
|
|
/* wip
|
|
var buf = new Uint8Array(32);
|
|
crypto.getRandomValues(buf);
|
|
const key = aesjs.utils.hex.fromBytes(Array.from(buf));
|
|
// sign it to each of the members
|
|
for (let i = 0; i < newmembers.length; i++) {
|
|
const member = newmembers[i];
|
|
const sig = await openpgp.encrypt({
|
|
message: await openpgp.createMessage({ text: key }),
|
|
signingKeys: window.keys.priv,
|
|
});
|
|
}
|
|
*/
|
|
}
|
|
window.emit("create_thread", {
|
|
name: document.getElementById("newthreadname").value,
|
|
permissions,
|
|
members,
|
|
}, msg => {
|
|
chooseThread({
|
|
name: document.getElementById("newthreadname").value,
|
|
id: msg.id,
|
|
});
|
|
document.getElementById('createthread').remove();
|
|
document.getElementById("loadmore").classList.add("hidden");
|
|
document.getElementById("msginput").classList.remove("hidden");
|
|
});
|
|
}
|
|
|
|
function sendMessage(e) {
|
|
e.preventDefault();
|
|
const msg = document.getElementById("msg").value;
|
|
if (!msg)
|
|
return;
|
|
window.emit("send_message", {
|
|
message: msg,
|
|
thread: window.currentThreadId,
|
|
});
|
|
document.getElementById("msg").value = "";
|
|
}
|
|
|
|
function newThread(e) {
|
|
let form = document.getElementById('createthread');
|
|
if (form) {
|
|
form.remove();
|
|
e.target.textContent = 'create thread';
|
|
}
|
|
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>
|
|
<input type="text" id="newthreadname" />
|
|
<p id='permissions'>thread permissions</p>
|
|
<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">
|
|
<p class='member'>${window.name}</p>
|
|
</div>
|
|
<br />
|
|
<input id="submitthread" type="submit" value="create" />
|
|
</form>
|
|
`);
|
|
e.target.textContent = 'cancel';
|
|
}
|
|
}
|
|
|
|
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>
|
|
<button id='newthread' onclick=${newThread}>create thread</button>
|
|
</div>
|
|
<div id="chat" class="column">
|
|
<h3 class="thread">
|
|
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;
|
|
document.getElementById("messages").appendChild(html.node`
|
|
<div class='message'>
|
|
<strong>${msg.name}: </strong>
|
|
${msg.message}
|
|
</div>`);
|
|
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 };
|
|
await auth();
|
|
document.getElementById('app').classList.remove('hidden');
|
|
}
|
|
else
|
|
document.getElementById('register').classList.remove('hidden');
|
|
};
|