Skip to content

Commit

Permalink
Implement Blossom.
Browse files Browse the repository at this point in the history
  • Loading branch information
ibz committed May 19, 2024
1 parent 3dbc791 commit 1186e06
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 98 deletions.
3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ edition = "2021"
async-std = { version = "1", features = ["attributes"] }
base64 = { version = "0.21" }
bitcoin_hashes = { version = "0.12", features = ["serde"] }
bytes = "1.5"
chrono = { version = "0", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
femme = "2"
futures-util = "0.3"
http-types = "2"
lazy_static = "1.4"
markdown = "1.0.0-alpha.3"
mime_guess = "2.0.4"
multer = "3.0.0"
phf = { version = "0.11.2", features = ["macros"] }
regex = "1"
secp256k1 = { version = "0.27", features = ["serde", "bitcoin_hashes"] }
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## About

**Servus** is a **CMS** and **personal Nostr relay** fully self-contained within one executable file.
**Servus** is a **CMS**, **personal Nostr relay** and **personal Blossom server** fully self-contained within one executable file.

### What?! And why??

Expand All @@ -22,11 +22,15 @@ Unlike Jekyll, **Servus** does not have a build step, does not require nginx or

#### Personal Nostr Relay

While I think the Nostr protocol is a step forward from RSS/Atom and ActivityPub and will eventually supersede both, writing apps that make use of the Nostr protocol is still widely misunderstood. People use Nostr clients to post to Nostr relays they have absolutely no control over. This model may work for Twitter clones or messaging apps where you don't need long term persistence of the content... But I want to be in control of my own data and know my data is there to stay.
While I think the Nostr protocol is a step forward from RSS/Atom and ActivityPub and will eventually supersede both, writing apps that make use of the Nostr protocol is still widely misunderstood. People use Nostr clients to post to Nostr relays they have absolutely no control over. This model may work for certain use cases where you don't need long term persistence of the content... But I want to be in control of my own data and know my data is there to stay.

**Servus** is the "canonical" source of my data, which it exposes as a Nostr relay.

Always remember, the *T* in Nostr stands for "transmitted". Relays are used to *relay* information, not to *store* information!
Always remember, the *T* in Nostr stands for "transmitted". Relays are used to *relay* your data, not to *store* it!

#### Personal Blossom Server

A CMS is incomplete if all it can deal with is text. It goes without saying that if you want to be in control of your data, that includes your images. Servus acts as your personal [Blossom](https://github.com/hzrd149/blossom) server.

## Goals and non-goals

Expand Down Expand Up @@ -199,6 +203,10 @@ A `GET` to `https://<ADMIN_DOMAIN>/api/sites` can be used to get a list of all t

NB: Both requests require a [NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md) authorization header to be present, which will be validated and used to decide which Nostr pubkey the request is referring to!

## Blossom API

Servus also implements the [Blossom API](https://github.com/hzrd149/blossom) and therefore acts as your personal Blossom server.

## Admin interface

The same `--admin-domain <ADMIN_DOMAIN>` flag used to activate the REST API is also used to activate... you guessed it... the *admin interface*!
Expand Down
62 changes: 45 additions & 17 deletions admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
return `Nostr ${btoa(JSON.stringify(authEvent))}`;
}

async function getBlossomAuthHeader(method) {
let authEvent = await getEvent(24242, "", [['t', method]]);
return `Nostr ${btoa(JSON.stringify(authEvent))}`;
}

async function getSites(sites) {
let endpoint = `${API_BASE_URL}/api/sites`;
sites.length = 0;
Expand Down Expand Up @@ -128,32 +133,45 @@
};
}

async function getFiles(site, files) {
files.length = 0;

const res = await fetch(new URL(`${window.location.protocol}//${site.domain}/list/${await window.nostr.getPublicKey()}`));
for (f of await res.json()) {
files.push(f);
}
}

function saveNote(note) {
let ws = new WebSocket(`${WS_PROTOCOL}//${note.site.domain}`);
ws.onopen = async (e) => {
ws.send(JSON.stringify(['EVENT', await getEvent(EVENT_KIND_NOTE, note.content, [])]));
};
}

async function uploadFile(site) {
const endpoint = `${window.location.protocol}//${site.domain}/api/files`;
const request = new XMLHttpRequest();
const formData = new FormData();
request.open("POST", endpoint, true);
request.setRequestHeader('Authorization', await getNostrAuthHeader(endpoint, 'POST'));
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 201) {
alert("Upload successful!");
}
};
async function uploadFileBlossom(site) {
const endpoint = `${window.location.protocol}//${site.domain}/upload`;
let fileInput = document.querySelector('#fileInput');
formData.append('file', fileInput.files[0])
request.send(formData);
const res = await fetch(new URL(endpoint), {
method: "PUT",
body: fileInput.files[0],
headers: { authorization: await getBlossomAuthHeader('upload') },
});
return (await res.json());
}

async function deleteFile(site, sha256) {
let endpoint = `${window.location.protocol}//${site.domain}/${sha256}`;
await fetch(new URL(endpoint),
{
method: 'DELETE',
headers: { authorization: await getBlossomAuthHeader('delete') }
});
}
</script>
</head>
<body>
<div class="w-full mx-auto" x-data="{site: null, post: null, note: null, noteManager: false, fileManager: false, sites: [], posts: [], notes: []}" x-init="await getSites(sites); await getPosts(sites, posts)">
<div class="w-full mx-auto" x-data="{site: null, post: null, note: null, noteManager: false, fileManager: false, sites: [], posts: [], notes: [], files: []}">
<div class="navbar bg-base-200">
<div class="navbar-start">
<a class="btn btn-ghost normal-case text-xl">Servus</a>
Expand Down Expand Up @@ -247,14 +265,24 @@
</template>
<template x-if="site && !post && !noteManager && fileManager">
<div>
<input type="file" id="fileInput" />
<button x-on:click="await uploadFile(site);" class="btn btn-primary">Upload</button>
<ul class="list-disc">
<template x-for="f in files">
<li>
<span x-text="f.sha256"></span> (<span x-text="Math.floor(f.size / 1024)"></span> kb) <button class="btn btn-error" x-on:click="if (confirm('Are you sure?')) { await deleteFile(site, f.sha256); getFiles(site, files); } ">Delete</button>
</li>
</template>
</ul>
<div>
<input type="file" id="fileInput" />
<button x-on:click="await uploadFileBlossom(site); await getFiles(site, files);" class="btn btn-primary">Upload</button>
</div>
</div>
</template>
</div>
<div class="drawer-side">
<label for="admin-drawer" class="drawer-overlay"></label>
<ul class="menu p-4 w-80 min-h-full bg-base-200 text-base-content">
<li class="mb-2"><button x-on:click="await getSites(sites); await getPosts(sites, posts);" class="btn btn-primary">Refresh</button></li>
<template x-for="s in sites">
<div class="collapse collapse-arrow bg-base-200">
<input type="radio" name="admin-accordion" />
Expand All @@ -278,7 +306,7 @@
</ul>
<a class="btn btn-outline btn-primary" x-on:click="site = s; post = {'id': undefined, 'title': 'New post', 'content': 'New content', 'site': s, 'persisted': false}; posts = posts.concat(post); noteManager = false; fileManager = false;">New post</a>
<a class="btn btn-outline btn-primary" x-on:click="site = s; post = null; noteManager = true; fileManager = false; getNotes(site, notes); note = {'content': '', 'site': s}">Notes</a>
<a class="btn btn-outline btn-primary" x-on:click="site = s; post = null; noteManager = false; fileManager = true;">Files</a>
<a class="btn btn-outline btn-primary" x-on:click="site = s; post = null; noteManager = false; fileManager = true; await getFiles(site, files);">Files</a>
</div>
</div>
</template>
Expand Down
Loading

0 comments on commit 1186e06

Please sign in to comment.