add webserver, sbot follow and subscription logic for database

This commit is contained in:
glyph 2022-05-10 16:14:28 +02:00
parent 51521a57f9
commit 12b4165029
6 changed files with 255 additions and 0 deletions

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "lykin"
version = "0.1.0"
edition = "2021"
[dependencies]
async-std = "1.10"
bincode = "1.3"
env_logger = "0.9"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
log = "0.4"
rocket = "0.5.0-rc.1"
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["tera"] }
sled = "0.34"

84
src/db.rs Normal file
View File

@ -0,0 +1,84 @@
// Key-value store using sled.
use std::{
fmt,
fmt::{Display, Formatter},
path::Path,
};
use bincode::Options;
use log::{debug, info, warn};
use sled::{Db, IVec, Result, Tree};
#[derive(Debug, Clone, PartialEq, Eq)]
struct IVecString {
pub bytes: IVec,
pub string: String,
}
impl Display for IVecString {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.string)
}
}
impl From<IVec> for IVecString {
fn from(bytes: IVec) -> Self {
Self {
string: String::from_utf8_lossy(&bytes).into_owned(),
bytes,
}
}
}
pub struct Database {
/// Stores the sled database instance.
db: Db,
/// Stores the public keys of all the feeds we are subscribed to.
feed_tree: Tree,
/// Stores the messages (content and metadata) for all the feeds we are subscribed to.
message_tree: Tree,
}
impl Database {
// TODO: return Result<Self> and use try operators
// implement simple custom error type
pub fn init(path: &Path) -> Self {
// Open the database at the given path.
// The database will be created if it does not yet exist.
// This code will panic if an IO error is encountered.
info!("initialising the sled database");
let db = sled::open(path).expect("failed to open database");
debug!("opening the 'feeds' database tree");
let feed_tree = db
.open_tree("feeds")
.expect("failed to open database feeds tree");
debug!("opening the 'messages' database tree");
let message_tree = db
.open_tree("messages")
.expect("failed to open database messages tree");
Database {
db,
feed_tree,
message_tree,
}
}
pub fn add_feed(&self, public_key: &str) -> Result<Option<IVec>> {
self.feed_tree.insert(&public_key, vec![0])
}
pub fn remove_feed(&self, public_key: &str) -> Result<Option<IVec>> {
self.feed_tree.remove(&public_key)
}
pub fn get_feeds(&self) -> Vec<String> {
self.feed_tree
.iter()
.keys()
.map(|bytes| IVecString::from(bytes.unwrap()))
.map(|ivec_string| ivec_string.string)
.collect()
}
}

79
src/main.rs Normal file
View File

@ -0,0 +1,79 @@
mod db;
mod sbot;
mod utils;
use std::path::Path;
use log::{debug, info, warn};
use rocket::{form::Form, get, launch, post, response::Redirect, routes, uri, FromForm, State};
use rocket_dyn_templates::{tera::Context, Template};
use crate::db::Database;
#[derive(FromForm)]
struct Peer {
public_key: String,
}
#[get("/")]
fn index(db: &State<Database>) -> Template {
let mut context = Context::new();
let feeds = db.get_feeds();
context.insert("feeds", &feeds);
Template::render("index", &context.into_json())
}
#[post("/subscribe", data = "<peer>")]
fn subscribe_form(db: &State<Database>, peer: Form<Peer>) -> Redirect {
// validate the public key
if let Ok(_) = utils::validate_public_key(&peer.public_key) {
debug!("public key {} is valid", &peer.public_key);
match db.add_feed(&peer.public_key) {
Ok(_) => {
debug!("added {} to feed tree in database", &peer.public_key);
// check if we already follow the peer
// - if not, follow the peer and create a tree for the peer
}
Err(_e) => warn!(
"failed to add {} to feed tree in database",
&peer.public_key
),
}
} else {
warn!("{} is invalid", &peer.public_key);
}
Redirect::to(uri!(index))
}
#[post("/unsubscribe", data = "<peer>")]
fn unsubscribe_form(db: &State<Database>, peer: Form<Peer>) -> Redirect {
// validate the public key
match utils::validate_public_key(&peer.public_key) {
Ok(_) => {
debug!("public key {} is valid", &peer.public_key);
match db.remove_feed(&peer.public_key) {
Ok(_) => debug!("removed {} from feed tree in database", &peer.public_key),
Err(_e) => warn!(
"failed to remove {} from feed tree in database",
&peer.public_key
),
}
}
Err(e) => warn!("{} is invalid: {}", &peer.public_key, e),
}
Redirect::to(uri!(index))
}
#[launch]
fn rocket() -> _ {
env_logger::init();
info!("launching the web server");
rocket::build()
.manage(Database::init(Path::new("lykin")))
.mount("/", routes![index, subscribe_form, unsubscribe_form])
.attach(Template::fairing())
}

16
src/sbot.rs Normal file
View File

@ -0,0 +1,16 @@
// Scuttlebutt functionality.
use async_std::task;
use golgi::Sbot;
/// Follow a peer.
pub fn follow_peer(public_key: &str) -> Result<String, String> {
task::block_on(async {
let mut sbot_client = Sbot::init(None, None).await.map_err(|e| e.to_string())?;
match sbot_client.follow(public_key).await {
Ok(_) => Ok("Followed peer".to_string()),
Err(e) => Err(format!("Failed to follow peer: {}", e)),
}
})
}

30
src/utils.rs Normal file
View File

@ -0,0 +1,30 @@
/// Ensure that the given public key is a valid ed25519 key.
///
/// Return an error string if the key is invalid.
pub fn validate_public_key(public_key: &str) -> Result<(), String> {
// ensure the id starts with the correct sigil link
if !public_key.starts_with('@') {
return Err("expected '@' sigil as first character".to_string());
}
// find the dot index denoting the start of the algorithm definition tag
let dot_index = match public_key.rfind('.') {
Some(index) => index,
None => return Err("no dot index was found".to_string()),
};
// check hashing algorithm (must end with ".ed25519")
if !&public_key.ends_with(".ed25519") {
return Err("hashing algorithm must be ed25519".to_string());
}
// obtain the base64 portion (substring) of the public key
let base64_str = &public_key[1..dot_index];
// length of a base64 encoded ed25519 public key
if base64_str.len() != 44 {
return Err("base64 data length is incorrect".to_string());
}
Ok(())
}

32
templates/index.html.tera Normal file
View File

@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>lykin</title>
<meta name="description" content="lykin: an SSB tutorial application">
<meta name="author" content="glyph">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>lykin</h1>
<form action="/subscribe" method="post" style="width: 500px;">
<fieldset>
<legend>Subscription Management</legend>
<input type="text" id="public_key" name="public_key" size=53 maxlength=53><br>
<label for="public_key">Public Key</label><br>
<br>
<input type="submit" value="Subscribe">
<input type="submit" value="Unsubscribe" formaction="/unsubscribe">
</fieldset>
</form>
<div>
<h2>Subscriptions</h2>
<ul>
{% for feed in feeds -%}
<li>{{ feed }}</li>
{%- endfor %}
</ul>
</div>
</body>
</html>