Compare commits

..

1 Commits
main ... main

Author SHA1 Message Date
jerl e34a8e7a2b celi 2025-06-27 02:31:56 -05:00
18 changed files with 102 additions and 93 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
node_modules
vybe.db
celi.db

View File

@ -1,7 +1,7 @@
# NOTICE: this document is outdated
# todo: update this document
# vybe websocket protocol - sent by client
# celi websocket protocol - sent by client
socket.io actions + expected msg format and other info
@ -18,7 +18,7 @@ if a message fails, you get a response with the following format:
## authenticate
you must generate a random salt, sign a message `vybe_auth [salt]`, and then send
you must generate a random salt, sign a message `celi_auth [salt]`, and then send
```json
{

View File

@ -1,9 +1,9 @@
## vybe
## celi
vybe is a work-in-progress decentralized **communication network**.
celi is a work-in-progress decentralized **communication network**.
a vybe instance (server) features user accounts and **threads**.
a celi instance (server) features user accounts and **threads**.
user accounts are owned by PGP keys stored in users' clients.
@ -30,7 +30,7 @@ after first run, the port is configured in instance.json.
**currently, you'll need to proxy it through a webserver like nginx or Caddy, which needs to have https enabled** for things to work correctly. if you want the easy option, i recommend Caddy. example Caddy config:
> ```
> vybe.example.domain {
> celi.example.domain {
> reverse_proxy localhost:1312
> header Access-Control-Allow-Origin "*"
> }
@ -38,13 +38,14 @@ after first run, the port is configured in instance.json.
(the allow-origin header isn't necessarily required but works as a fallback in case websockets don't work for whatever reason)
then go to `https://vybe.example.domain` to start using vybe!
then go to `https://celi.example.domain` to start using celi!
## todo
- encrypt private threads (users already use pgp)
- video in calls
- instance administration and moderation
- notifications
let me know if you have any questions or issues. i can be reached via my website [jerl.zone](https://jerl.zone).
if you want to contribute, contact me :)

View File

@ -219,7 +219,7 @@ document.body.append(html.node`
</div>
<hr class='separator' color='#505050'>
<div id='home' class='column'>
<h3>vybe</h3>
<h3>celi</h3>
<p id='instances' class='header'>instances:<button onclick=${e => {
let div = html.node`
<div>

View File

@ -103,11 +103,11 @@ async function submit(event) {
render(document.body, html`
<div id='register' class='hidden'>
<h1>welcome to vybe</h1>
<h1>welcome to celi</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
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>
<p>

View File

@ -8,7 +8,7 @@
<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>
<title>celi</title>
<style>
* {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
@ -127,16 +127,14 @@
margin-block: 5px;
word-wrap: break-word;
}
.thread:hover,
.tab:hover,
.instancetitle > span:hover,
#user:hover {
background-color: #333;
}
.thread.active,
.tab.active,
button:hover {
background-color: #4f4f4f;
background-color: #484848;
color: #fff;
}
label.heading {
@ -253,6 +251,13 @@
overflow-y: auto;
}
.thread {
&:hover {
background-color: #303030;
}
&.active {
background-color: #404040;
color: #fff;
}
padding: 2px 3px;
white-space: pre;
cursor: default;
@ -293,9 +298,9 @@
}
.tab {
padding: 5px 7px;
background-color: #1f1f1f;
background-color: #222;
border: 0;
color: #ddd;
color: #e0e0e0;
font-weight: 500;
}
.tabcontent {

View File

@ -22,7 +22,7 @@ function loadMessages(callback) {
});
msg.value = '';
}}>
<input id='msg' placeholder='write a message...' />
<input id='msg' placeholder='write a message...' autocomplete='off' />
<button type='submit' id='sendmsg'>send</button>
</form>
`);

View File

@ -17,11 +17,14 @@ function setVisibility() {
function openThread(div, pushState) {
if (!document.getElementById('removemember').classList.contains('hidden'))
document.getElementById('member').classList.add('hidden');
if (window.currentThread)
window.currentThread.div.classList.remove('active');
if (!div) {
document.getElementById('thread').classList.add('hidden');
window.currentThread = null;
return;
}
div.classList.add('active');
document.getElementById('thread').classList.remove('hidden');
const edit = document.getElementById('edit');
if (div.thread.permissions.admin) {
@ -32,9 +35,6 @@ function openThread(div, pushState) {
document.getElementById('membersadd').classList.add('hidden');
}
document.getElementById('threadname').textContent = div.thread.name;
if (window.currentThread)
window.currentThread.div.classList.remove('active');
div.classList.add('active');
window.currentThread = div.thread;
window.currentInstance = div.thread.instance;
window.currentInstance.emit('get_thread', { thread: div.thread.id }, async msg => {
@ -487,9 +487,9 @@ async function loadThreads(instancediv, select) {
if (el) {
Object.assign(el.thread, thread);
el.children['name'].textContent = thread.name;
if (!thread.permissions.everyone.view.value && !el.children['membericon'])
if (!thread.permissions.public && !el.children['membericon'])
el.insertAdjacentHTML('beforeend', `<img id='membericon' src='/members.png'>`);
else if (el.children['membericon'] && thread.permissions.everyone.value)
else if (el.children['membericon'] && thread.permissions.public)
el.children['membericon'].remove();
if (window.currentThread?.id === thread.id) {
Object.assign(window.currentThread, thread);

View File

@ -1,5 +1,5 @@
{
"name": "vybe",
"name": "celi",
"version": "1.0.0",
"description": "",
"main": "server.js",

View File

@ -46,7 +46,7 @@ if (!instance.url)
it will be auto-set by the first user authentication !
it can be manually set in instance.json .`);
global.vybe = {
global.celi = {
instance,
saveSettings,
instances: {},
@ -55,7 +55,7 @@ global.vybe = {
calls: {}, // rtc peer connections go in here
streams: {},
connectInstance: async url => {
let instance = vybe.instances[url];
let instance = celi.instances[url];
function connecting(resolve, reject) {
instance.socket.on('connect', resolve);
instance.socket.on('connect_error', error => {
@ -70,7 +70,7 @@ global.vybe = {
}
}
else {
instance = vybe.instances[url] = {
instance = celi.instances[url] = {
socket: ioclient('https://' + url)
};
await new Promise(connecting);
@ -92,7 +92,7 @@ app.use(express.static('client'));
// todo: secure this
app.get('/stream/:id', (req, res) => {
let stream = vybe.streams[req.params.id];
let stream = celi.streams[req.params.id];
if (!stream) {
res.sendStatus(404);
return;
@ -121,17 +121,17 @@ io.on('connection', (socket) => {
});
}
socket.on('disconnect', async reason => {
let user = vybe.users[socket.__userid];
let user = celi.users[socket.__userid];
if (user)
user.sockets.splice(user.sockets.indexOf(socket), 1);
for (let id in vybe.streams) {
const stream = vybe.streams[id];
for (let id in celi.streams) {
const stream = celi.streams[id];
delete stream.listeners[socket.id];
if (stream.socket === socket)
stream.stop();
}
for (let id in vybe.calls) {
let call = vybe.calls[id];
for (let id in celi.calls) {
let call = celi.calls[id];
let connection = call[socket.__userid];
if (!connection)
continue;
@ -141,8 +141,8 @@ io.on('connection', (socket) => {
}
connection.close();
delete call[id];
delete vybe.threads[id].call[socket.__userid];
await vybe.threads[id].emitcall();
delete celi.threads[id].call[socket.__userid];
await celi.threads[id].emitcall();
}
});
});

View File

@ -11,7 +11,7 @@ const authwrap = (fn) => async (msg, respond, socket) => {
}
return await fn({
...msg,
auth_user: vybe.users[socket.__userid]
auth_user: celi.users[socket.__userid]
}, respond, socket);
};

View File

@ -1,12 +1,12 @@
const sqlite3 = require('sqlite3');
const fs = require('fs');
const path = 'vybe.db';
const path = 'celi.db';
const existed = fs.existsSync(path);
const db = new sqlite3.Database(path);
db.query = function (sql, params) {
db.query = function(sql, params) {
return new Promise((resolve, reject) => {
db.all(sql, params, (error, rows) => {
if (error)
@ -17,11 +17,12 @@ db.query = function (sql, params) {
});
};
(async () => {
db.ready = new Promise(async resolve => {
if (!existed)
for (let sql of fs.readFileSync('./db/1-init.sql').toString().split(';'))
if (sql.trim())
await db.query(sql);
})();
resolve();
});
module.exports = db;

View File

@ -15,8 +15,8 @@ async function join(msg, respond, socket) {
success: false,
message: "user doesn't have permission"
});
const thread = vybe.threads[msg.thread];
const call = vybe.calls[msg.thread];
const thread = celi.threads[msg.thread];
const call = celi.calls[msg.thread];
let connection = call[msg.auth_user.id];
if (connection) {
if (connection.track) {
@ -66,8 +66,8 @@ async function join(msg, respond, socket) {
connection.connected = true;
thread.call[msg.auth_user.id] = {
id: msg.auth_user.id,
name: vybe.users[msg.auth_user.id].name,
displayname: vybe.users[msg.auth_user.id].displayname,
name: celi.users[msg.auth_user.id].name,
displayname: celi.users[msg.auth_user.id].displayname,
permissions: Object.fromEntries((await db.query(
`select permission, value from permission
where type = 'user' and thread = ? and user = ?`,
@ -104,7 +104,7 @@ async function offer(msg, respond) {
success: false,
message: 'missing argument'
});
let connection = vybe.calls[msg.thread]?.[msg.auth_user.id];
let connection = celi.calls[msg.thread]?.[msg.auth_user.id];
if (!connection)
return respond({
success: false,
@ -124,7 +124,7 @@ async function ice(msg, respond) {
success: false,
message: 'missing argument'
});
let connection = vybe.calls[msg.thread]?.[msg.auth_user.id];
let connection = celi.calls[msg.thread]?.[msg.auth_user.id];
if (!connection)
return respond({
success: false,
@ -144,8 +144,8 @@ async function ice(msg, respond) {
}
async function leave(msg, respond) {
let thread = vybe.threads[msg.thread];
let call = vybe.calls[msg.thread];
let thread = celi.threads[msg.thread];
let call = celi.calls[msg.thread];
let connection = call?.[msg.auth_user.id];
if (!connection)
return respond({

View File

@ -33,10 +33,10 @@ async function send_message(msg, respond) {
if (p.type === 'user' && p.user == msg.auth_user.id)
userperms[p.permission] = ['admin', 'post', 'view'].includes(p.permission)
? p.value === 'true' : p.value;
for (let id in vybe.users) {
for (let id in celi.users) {
if (perms.find(p => p.type === 'everyone' && p.permission === 'view' && p.value === 'true')
|| members.includes(id)) {
for (let s of vybe.users[id].sockets) {
for (let s of celi.users[id].sockets) {
s.emit('new_message', {
id,
user: {

View File

@ -65,9 +65,9 @@ async function save_span(msg, respond, socket) {
and type = 'everyone' and value = 'true' and permission = 'view'`,
msg.thread
);
for (let userid in vybe.users) {
for (let userid in celi.users) {
if (permissions.rows.length > 0 || members.includes(userid)) {
for (let s of vybe.users[userid].sockets) {
for (let s of celi.users[userid].sockets) {
if (s !== socket)
s.emit('span', {
id,

View File

@ -17,14 +17,14 @@ async function stream(msg, respond, socket) {
`select * from permission
where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'`,
msg.thread)).rows.length) {
for (let id in vybe.users)
for (let socket of vybe.users[id].sockets)
for (let id in celi.users)
for (let socket of celi.users[id].sockets)
socket.emit('stream', stream);
} else {
for (let member of (
await db.query('select user from member where member.thread = ?', msg.thread)
).rows) {
member = vybe.users[member.user];
member = celi.users[member.user];
if (member)
for (let socket of member.sockets)
socket.emit('stream', stream);
@ -32,7 +32,7 @@ async function stream(msg, respond, socket) {
}
}
if (typeof msg.id === 'number') {
let vstream = vybe.streams[msg.id];
let vstream = celi.streams[msg.id];
if (!vstream)
return respond({
success: false,
@ -68,9 +68,9 @@ async function stream(msg, respond, socket) {
},
name: msg.name
};
let thread = vybe.threads[msg.thread];
let thread = celi.threads[msg.thread];
thread.streams.push(stream);
vybe.streams[stream.id] = {
celi.streams[stream.id] = {
stream,
userid: msg.auth_user.id,
listeners: {},
@ -78,7 +78,7 @@ async function stream(msg, respond, socket) {
stop: async () => {
stream.stopped = true;
thread.streams.splice(thread.streams.findIndex(s => s.id === stream.id), 1);
delete vybe.streams[stream.id];
delete celi.streams[stream.id];
await send();
},
streams: new Set()
@ -92,7 +92,7 @@ async function stream(msg, respond, socket) {
}
async function streamdata(msg, respond) {
let stream = vybe.streams[msg.id];
let stream = celi.streams[msg.id];
if (!stream) {
return respond({
success: false,
@ -119,7 +119,7 @@ async function play_stream(msg, respond, socket) {
message: "user doesn't have permission"
});
}
let stream = vybe.streams[msg.id];
let stream = celi.streams[msg.id];
if (!stream)
return respond({
success: false,

View File

@ -2,11 +2,11 @@ const db = require('../db');
const authwrap = require('../authwrap');
const check_permission = require('../check_permission');
(async () => {
db.ready.then(async () => {
for (let thread of (await db.query(
`select name, id from thread`
)).rows) {
vybe.threads[thread.id] = {
celi.threads[thread.id] = {
...thread,
streams: [],
call: {}, // list of members in call
@ -19,14 +19,14 @@ const check_permission = require('../check_permission');
`select * from permission
where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'`,
this.id)).rows.length) {
for (let id in vybe.users)
for (let socket of vybe.users[id].sockets)
for (let id in celi.users)
for (let socket of celi.users[id].sockets)
socket.emit('call', msg);
} else {
for (let member of (
await db.query('select user from member where member.thread = ?', this.id)
).rows) {
member = vybe.users[member.user];
member = celi.users[member.user];
if (member)
for (let socket of member.sockets)
socket.emit('call', msg);
@ -34,9 +34,9 @@ const check_permission = require('../check_permission');
}
}
};
vybe.calls[thread.id] = {};
celi.calls[thread.id] = {};
}
})();
});
async function create_thread(msg, respond) {
// validate inputs
@ -101,7 +101,7 @@ async function create_thread(msg, respond) {
let members = {};
for (let member of msg.members) {
let id = member.id.split('@');
id = id[1] === vybe.instance.url ? id[0] : member.id;
id = id[1] === celi.instance.url ? id[0] : member.id;
if (members[id])
continue;
await db.query(
@ -116,19 +116,20 @@ async function create_thread(msg, respond) {
values (?, ?, ?, ?, ?, ?)`,
[thread_id, 'user', id, true, permission, String(member.permissions[permission])]);
}
let thread = vybe.threads[thread_id] = {
let thread = celi.threads[thread_id] = {
id: thread_id,
name: msg.name,
streams: [],
call: {}
};
vybe.calls[thread_id] = {};
celi.calls[thread_id] = {};
if (!msg.permissions?.view_limited) {
for (let id in vybe.users) {
for (let socket of vybe.users[id].sockets) {
for (let id in celi.users) {
for (let socket of celi.users[id].sockets) {
socket.emit('thread', {
...thread,
permissions: {
public: true,
view: true,
post: !msg.permissions?.post_limited || members[id],
admin: id === msg.auth_user.id
@ -139,7 +140,7 @@ async function create_thread(msg, respond) {
}
else {
for (let member in members) {
member = vybe.users[member];
member = celi.users[member];
if (!member)
continue;
for (let socket of member.sockets) {
@ -180,7 +181,7 @@ async function list_threads(msg, respond) {
success: true,
threads: await Promise.all(threads.rows.map(async row => {
let thread = {};
Object.assign(thread, vybe.threads[row.id]);
Object.assign(thread, celi.threads[row.id]);
thread.permissions = await check_permission(msg.auth_user.id, row.id);
thread.permissions.public = row.value === 'true';
return thread;
@ -248,7 +249,7 @@ async function get_thread(msg, respond) {
return respond({
success: true,
thread: {
...vybe.threads[msg.thread],
...celi.threads[msg.thread],
permissions: perms,
members: Object.values(members)
}
@ -276,7 +277,7 @@ async function edit_thread(msg, respond) {
message: "user doesn't have permission"
});
}
let thread = vybe.threads[msg.id];
let thread = celi.threads[msg.id];
// update name
await db.query(
'update thread set name = ? where id = ?',
@ -368,11 +369,12 @@ async function edit_thread(msg, respond) {
msg.id
)).rows.map(row => [row.user, true]));
if (!msg.permissions?.view_limited) {
for (let id in vybe.users)
for (let socket of vybe.users[id].sockets)
for (let id in celi.users)
for (let socket of celi.users[id].sockets)
socket.emit('thread', {
...thread,
permissions: {
public: true,
view: true,
post: !msg.permissions?.post_limited || id in members,
admin: id === msg.auth_user.id && perms.admin,
@ -382,7 +384,7 @@ async function edit_thread(msg, respond) {
}
else {
for (let member in members) {
member = vybe.users[member];
member = celi.users[member];
if (!member)
continue;
for (let socket of member.sockets)

View File

@ -85,7 +85,7 @@ async function authenticate(msg, respond, socket) {
expectSigned: true,
date: new Date(Date.now() + 60000 * 4) // slightly in the future to compensate for some system clocks
});
if (vybe.instance.url === id[1] || (!vybe.instance.url && !id[0])) {
if (celi.instance.url === id[1] || (!celi.instance.url && !id[0])) {
// this should be user's home instance
user = await getUser(id[0], msg.name);
if (!user) {
@ -102,13 +102,13 @@ async function authenticate(msg, respond, socket) {
);
if (result.rows.length === 0) {
// request auth from logged in sessions
if (!vybe.users[user.id])
vybe.users[user.id] = {
if (!celi.users[user.id])
celi.users[user.id] = {
...user,
sockets: [],
authrequests: {}
};
user = vybe.users[user.id];
user = celi.users[user.id];
let id = key.getFingerprint().slice(0, 8);
let time = Date.now();
if (!await new Promise(resolve => {
@ -129,15 +129,15 @@ async function authenticate(msg, respond, socket) {
[user.id, pubkey]);
}
// default instance url to first authenticated user's location.host
if (!vybe.instance.url) {
vybe.instance.url = id[1];
vybe.saveSettings();
if (!celi.instance.url) {
celi.instance.url = id[1];
celi.saveSettings();
}
}
else {
// connect to user's home instance and ask for their key
try {
let instance = await vybe.connectInstance(id[1]);
let instance = await celi.connectInstance(id[1]);
await new Promise((resolve, reject) => {
instance.socket.emit('get_keys', { ids: [id[0]] }, msg => {
if (!msg.success) {
@ -164,7 +164,7 @@ async function authenticate(msg, respond, socket) {
// this socket is now authenticated
if (!user.displayname)
user.displayname = user.name;
user = vybe.users[user.id] || (vybe.users[user.id] = {
user = celi.users[user.id] || (celi.users[user.id] = {
...user,
sockets: [],
authrequests: {}
@ -175,7 +175,7 @@ async function authenticate(msg, respond, socket) {
respond({
success: true,
instance: {
id: vybe.instance.id
id: celi.instance.id
},
id: user.id,
name: user.name,
@ -198,7 +198,7 @@ async function authenticate(msg, respond, socket) {
}
async function authorize_key(msg, respond) {
let authrequest = vybe.users[msg.auth_user.id].authrequests[msg.id];
let authrequest = celi.users[msg.auth_user.id].authrequests[msg.id];
if (!authrequest) {
return respond({
success: false,
@ -219,8 +219,8 @@ async function update_user(msg, respond) {
await db.query(
`update user set displayname = ?, bio = ?, public = ? where id = ?`,
[msg.displayname, msg.bio, !!msg.public, msg.auth_user.id]);
vybe.users[msg.auth_user.id].displayname = msg.displayname;
vybe.users[msg.auth_user.id].bio = msg.bio;
celi.users[msg.auth_user.id].displayname = msg.displayname;
celi.users[msg.auth_user.id].bio = msg.bio;
respond({ success: true });
}