create ui and task loop
This commit is contained in:
parent
12b4165029
commit
5012910c25
|
@ -6,9 +6,14 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = "1.10"
|
async-std = "1.10"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
|
chrono = "0.4"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
futures = "0.3"
|
||||||
|
#golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||||
|
golgi = { path = "../golgi" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rocket = "0.5.0-rc.1"
|
rocket = "0.5.0-rc.1"
|
||||||
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["tera"] }
|
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["tera"] }
|
||||||
|
serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
sled = "0.34"
|
sled = "0.34"
|
||||||
|
|
102
src/db.rs
102
src/db.rs
|
@ -6,9 +6,19 @@ use std::{
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bincode::Options;
|
use log::{debug, info};
|
||||||
use log::{debug, info, warn};
|
use serde::{Deserialize, Serialize};
|
||||||
use sled::{Db, IVec, Result, Tree};
|
use sled::{Batch, Db, IVec, Result, Tree};
|
||||||
|
|
||||||
|
// The text and metadata of a Scuttlebutt root post.
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Post {
|
||||||
|
pub key: String,
|
||||||
|
pub text: String,
|
||||||
|
pub date: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
pub read: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct IVecString {
|
struct IVecString {
|
||||||
|
@ -31,54 +41,96 @@ impl From<IVec> for IVecString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
/// Stores the sled database instance.
|
/// Stores the sled database instance.
|
||||||
db: Db,
|
db: Db,
|
||||||
/// Stores the public keys of all the feeds we are subscribed to.
|
/// Stores the public keys of all the peers we are subscribed to.
|
||||||
feed_tree: Tree,
|
peer_tree: Tree,
|
||||||
/// Stores the messages (content and metadata) for all the feeds we are subscribed to.
|
/// Stores the posts (content and metadata) for all the feeds we are subscribed to.
|
||||||
message_tree: Tree,
|
pub post_tree: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
// TODO: return Result<Self> and use try operators
|
|
||||||
// implement simple custom error type
|
|
||||||
pub fn init(path: &Path) -> Self {
|
pub fn init(path: &Path) -> Self {
|
||||||
// Open the database at the given path.
|
// Open the database at the given path.
|
||||||
// The database will be created if it does not yet exist.
|
// The database will be created if it does not yet exist.
|
||||||
// This code will panic if an IO error is encountered.
|
// This code will panic if an IO error is encountered.
|
||||||
info!("initialising the sled database");
|
info!("initialising the sled database");
|
||||||
let db = sled::open(path).expect("failed to open database");
|
let db = sled::open(path).expect("failed to open database");
|
||||||
debug!("opening the 'feeds' database tree");
|
debug!("opening the 'peers' database tree");
|
||||||
let feed_tree = db
|
let peer_tree = db
|
||||||
.open_tree("feeds")
|
.open_tree("peers")
|
||||||
.expect("failed to open database feeds tree");
|
.expect("failed to open database peers tree");
|
||||||
debug!("opening the 'messages' database tree");
|
debug!("opening the 'posts' database tree");
|
||||||
let message_tree = db
|
let post_tree = db
|
||||||
.open_tree("messages")
|
.open_tree("posts")
|
||||||
.expect("failed to open database messages tree");
|
.expect("failed to open database posts tree");
|
||||||
|
|
||||||
Database {
|
Database {
|
||||||
db,
|
db,
|
||||||
feed_tree,
|
peer_tree,
|
||||||
message_tree,
|
post_tree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_feed(&self, public_key: &str) -> Result<Option<IVec>> {
|
pub fn add_peer(&self, public_key: &str) -> Result<Option<IVec>> {
|
||||||
self.feed_tree.insert(&public_key, vec![0])
|
self.peer_tree.insert(&public_key, vec![0])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_feed(&self, public_key: &str) -> Result<Option<IVec>> {
|
pub fn remove_peer(&self, public_key: &str) -> Result<Option<IVec>> {
|
||||||
self.feed_tree.remove(&public_key)
|
self.peer_tree.remove(&public_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_feeds(&self) -> Vec<String> {
|
pub fn get_peers(&self) -> Vec<String> {
|
||||||
self.feed_tree
|
self.peer_tree
|
||||||
.iter()
|
.iter()
|
||||||
.keys()
|
.keys()
|
||||||
.map(|bytes| IVecString::from(bytes.unwrap()))
|
.map(|bytes| IVecString::from(bytes.unwrap()))
|
||||||
.map(|ivec_string| ivec_string.string)
|
.map(|ivec_string| ivec_string.string)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_post(&self, public_key: &str, post: Post) -> Result<Option<IVec>> {
|
||||||
|
let post_key = format!("{}_{}", public_key, post.key);
|
||||||
|
let post_bytes = bincode::serialize(&post).unwrap();
|
||||||
|
|
||||||
|
self.post_tree.insert(post_key.as_bytes(), post_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_post_batch(&self, public_key: &str, posts: Vec<Post>) -> Result<()> {
|
||||||
|
let mut post_batch = Batch::default();
|
||||||
|
|
||||||
|
for post in posts {
|
||||||
|
let post_key = format!("{}_{}", public_key, post.key);
|
||||||
|
let post_bytes = bincode::serialize(&post).unwrap();
|
||||||
|
|
||||||
|
post_batch.insert(post_key.as_bytes(), post_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.post_tree.apply_batch(post_batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posts(&self, public_key: &str) -> Result<Vec<Post>> {
|
||||||
|
let mut posts = Vec::new();
|
||||||
|
|
||||||
|
self.post_tree
|
||||||
|
.scan_prefix(public_key.as_bytes())
|
||||||
|
.map(|post| post.unwrap())
|
||||||
|
.for_each(|post| posts.push(bincode::deserialize(&post.1).unwrap()));
|
||||||
|
|
||||||
|
Ok(posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_post(&self, public_key: &str, msg_id: &str) -> Result<Option<Post>> {
|
||||||
|
let post_key = format!("{}_{}", public_key, msg_id);
|
||||||
|
|
||||||
|
let post = self
|
||||||
|
.post_tree
|
||||||
|
.get(post_key.as_bytes())
|
||||||
|
.unwrap()
|
||||||
|
.map(|post| bincode::deserialize(&post).unwrap());
|
||||||
|
|
||||||
|
Ok(post)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
131
src/main.rs
131
src/main.rs
|
@ -1,14 +1,23 @@
|
||||||
mod db;
|
mod db;
|
||||||
mod sbot;
|
mod sbot;
|
||||||
|
mod task_loop;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::{env, path::Path};
|
||||||
|
|
||||||
|
use async_std::{channel, channel::Sender};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use rocket::{form::Form, get, launch, post, response::Redirect, routes, uri, FromForm, State};
|
use rocket::{
|
||||||
|
fairing::AdHoc,
|
||||||
|
form::Form,
|
||||||
|
fs::{relative, FileServer},
|
||||||
|
get, launch, post,
|
||||||
|
response::Redirect,
|
||||||
|
routes, uri, FromForm, State,
|
||||||
|
};
|
||||||
use rocket_dyn_templates::{tera::Context, Template};
|
use rocket_dyn_templates::{tera::Context, Template};
|
||||||
|
|
||||||
use crate::db::Database;
|
use crate::{db::Database, task_loop::Task};
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
struct Peer {
|
struct Peer {
|
||||||
|
@ -16,27 +25,77 @@ struct Peer {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
fn index(db: &State<Database>) -> Template {
|
async fn home(db: &State<Database>) -> Template {
|
||||||
let mut context = Context::new();
|
let peers = db.get_peers();
|
||||||
let feeds = db.get_feeds();
|
|
||||||
context.insert("feeds", &feeds);
|
|
||||||
|
|
||||||
Template::render("index", &context.into_json())
|
let mut context = Context::new();
|
||||||
|
context.insert("peers", &peers);
|
||||||
|
|
||||||
|
Template::render("home", &context.into_json())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/posts/<public_key>")]
|
||||||
|
async fn posts(db: &State<Database>, public_key: &str) -> Template {
|
||||||
|
let peers = db.get_peers();
|
||||||
|
let posts = db.get_posts(public_key).unwrap();
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("selected_peer", &public_key);
|
||||||
|
context.insert("peers", &peers);
|
||||||
|
context.insert("posts", &posts);
|
||||||
|
|
||||||
|
Template::render("home", &context.into_json())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/posts/<public_key>/<msg_id>")]
|
||||||
|
async fn post(db: &State<Database>, public_key: &str, msg_id: &str) -> Template {
|
||||||
|
let peers = db.get_peers();
|
||||||
|
let posts = db.get_posts(public_key).unwrap();
|
||||||
|
let post = db.get_post(public_key, msg_id).unwrap();
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("selected_peer", &public_key);
|
||||||
|
context.insert("selected_post", &msg_id);
|
||||||
|
context.insert("peers", &peers);
|
||||||
|
context.insert("posts", &posts);
|
||||||
|
context.insert("post", &post);
|
||||||
|
|
||||||
|
Template::render("home", &context.into_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/subscribe", data = "<peer>")]
|
#[post("/subscribe", data = "<peer>")]
|
||||||
fn subscribe_form(db: &State<Database>, peer: Form<Peer>) -> Redirect {
|
async fn subscribe_form(
|
||||||
// validate the public key
|
db: &State<Database>,
|
||||||
|
whoami: &State<WhoAmI>,
|
||||||
|
tx: &State<Sender<Task>>,
|
||||||
|
peer: Form<Peer>,
|
||||||
|
) -> Redirect {
|
||||||
if let Ok(_) = utils::validate_public_key(&peer.public_key) {
|
if let Ok(_) = utils::validate_public_key(&peer.public_key) {
|
||||||
debug!("public key {} is valid", &peer.public_key);
|
debug!("public key {} is valid", &peer.public_key);
|
||||||
match db.add_feed(&peer.public_key) {
|
match db.add_peer(&peer.public_key) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("added {} to feed tree in database", &peer.public_key);
|
debug!("added {} to peer tree in database", &peer.public_key);
|
||||||
// check if we already follow the peer
|
// TODO: i don't think we actually want to follow...
|
||||||
// - if not, follow the peer and create a tree for the peer
|
// we might still have the data in our ssb db, even if we don't follow
|
||||||
|
match sbot::is_following(&whoami.public_key, &peer.public_key).await {
|
||||||
|
Ok(status) if status.as_str() == "false" => {
|
||||||
|
match sbot::follow_peer(&peer.public_key).await {
|
||||||
|
Ok(_) => debug!("followed {}", &peer.public_key),
|
||||||
|
Err(e) => warn!("failed to follow {}: {}", &peer.public_key, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(status) if status.as_str() == "true" => {
|
||||||
|
debug!("we already follow {}", &peer.public_key)
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
let peer = peer.public_key.to_string();
|
||||||
|
if let Err(e) = tx.send(Task::FetchAll(peer)).await {
|
||||||
|
warn!("task loop error: {}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(_e) => warn!(
|
Err(_e) => warn!(
|
||||||
"failed to add {} to feed tree in database",
|
"failed to add {} to peer tree in database",
|
||||||
&peer.public_key
|
&peer.public_key
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -44,7 +103,7 @@ fn subscribe_form(db: &State<Database>, peer: Form<Peer>) -> Redirect {
|
||||||
warn!("{} is invalid", &peer.public_key);
|
warn!("{} is invalid", &peer.public_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Redirect::to(uri!(index))
|
Redirect::to(uri!(home))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/unsubscribe", data = "<peer>")]
|
#[post("/unsubscribe", data = "<peer>")]
|
||||||
|
@ -53,10 +112,10 @@ fn unsubscribe_form(db: &State<Database>, peer: Form<Peer>) -> Redirect {
|
||||||
match utils::validate_public_key(&peer.public_key) {
|
match utils::validate_public_key(&peer.public_key) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("public key {} is valid", &peer.public_key);
|
debug!("public key {} is valid", &peer.public_key);
|
||||||
match db.remove_feed(&peer.public_key) {
|
match db.remove_peer(&peer.public_key) {
|
||||||
Ok(_) => debug!("removed {} from feed tree in database", &peer.public_key),
|
Ok(_) => debug!("removed {} from peer tree in database", &peer.public_key),
|
||||||
Err(_e) => warn!(
|
Err(_e) => warn!(
|
||||||
"failed to remove {} from feed tree in database",
|
"failed to remove {} from peer tree in database",
|
||||||
&peer.public_key
|
&peer.public_key
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -64,16 +123,42 @@ fn unsubscribe_form(db: &State<Database>, peer: Form<Peer>) -> Redirect {
|
||||||
Err(e) => warn!("{} is invalid: {}", &peer.public_key, e),
|
Err(e) => warn!("{} is invalid: {}", &peer.public_key, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
Redirect::to(uri!(index))
|
Redirect::to(uri!(home))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WhoAmI {
|
||||||
|
public_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
|
let public_key: String = sbot::whoami().await.expect("whoami sbot call failed");
|
||||||
|
let whoami = WhoAmI { public_key };
|
||||||
|
|
||||||
|
let db = Database::init(Path::new("lykin_db"));
|
||||||
|
let db_clone = db.clone();
|
||||||
|
|
||||||
|
let (tx, rx) = channel::unbounded();
|
||||||
|
let tx_clone = tx.clone();
|
||||||
|
|
||||||
|
task_loop::spawn(rx, db_clone).await;
|
||||||
|
|
||||||
info!("launching the web server");
|
info!("launching the web server");
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.manage(Database::init(Path::new("lykin")))
|
.manage(db)
|
||||||
.mount("/", routes![index, subscribe_form, unsubscribe_form])
|
.manage(whoami)
|
||||||
|
.manage(tx)
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
routes![home, subscribe_form, unsubscribe_form, posts, post],
|
||||||
|
)
|
||||||
|
.mount("/", FileServer::from(relative!("static")))
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
|
.attach(AdHoc::on_shutdown("cancel task loop", |_| {
|
||||||
|
Box::pin(async move {
|
||||||
|
tx_clone.send(Task::Cancel).await;
|
||||||
|
})
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
107
src/sbot.rs
107
src/sbot.rs
|
@ -1,16 +1,103 @@
|
||||||
// Scuttlebutt functionality.
|
// Scuttlebutt functionality.
|
||||||
|
|
||||||
use async_std::task;
|
use async_std::stream::StreamExt;
|
||||||
use golgi::Sbot;
|
use chrono::{NaiveDate, NaiveDateTime, TimeZone, Utc};
|
||||||
|
use golgi::{
|
||||||
|
api::friends::RelationshipQuery,
|
||||||
|
messages::{SsbMessageContentType, SsbMessageKVT},
|
||||||
|
sbot::Keystore,
|
||||||
|
GolgiError, Sbot,
|
||||||
|
};
|
||||||
|
use log::warn;
|
||||||
|
use serde_json::value::Value;
|
||||||
|
|
||||||
|
use crate::db::Post;
|
||||||
|
|
||||||
|
pub async fn whoami() -> Result<String, String> {
|
||||||
|
let mut sbot = Sbot::init(Keystore::Patchwork, None, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
sbot.whoami().await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Follow a peer.
|
/// Follow a peer.
|
||||||
pub fn follow_peer(public_key: &str) -> Result<String, String> {
|
pub async fn follow_peer(public_key: &str) -> Result<String, String> {
|
||||||
task::block_on(async {
|
let mut sbot = Sbot::init(Keystore::Patchwork, None, None)
|
||||||
let mut sbot_client = Sbot::init(None, None).await.map_err(|e| e.to_string())?;
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
match sbot_client.follow(public_key).await {
|
sbot.follow(public_key).await.map_err(|e| e.to_string())
|
||||||
Ok(_) => Ok("Followed peer".to_string()),
|
}
|
||||||
Err(e) => Err(format!("Failed to follow peer: {}", e)),
|
|
||||||
}
|
/// Check follow status.
|
||||||
})
|
///
|
||||||
|
/// Is peer A (`public_key_a`) following peer B (`public_key_b`)?
|
||||||
|
pub async fn is_following(public_key_a: &str, public_key_b: &str) -> Result<String, String> {
|
||||||
|
let mut sbot = Sbot::init(Keystore::Patchwork, None, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let query = RelationshipQuery {
|
||||||
|
source: public_key_a.to_string(),
|
||||||
|
dest: public_key_b.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sbot.friends_is_following(query)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_message_stream(
|
||||||
|
public_key: &str,
|
||||||
|
) -> impl futures::Stream<Item = Result<SsbMessageKVT, GolgiError>> {
|
||||||
|
let mut sbot = Sbot::init(Keystore::Patchwork, None, None)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sbot.create_history_stream(public_key.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_root_posts(
|
||||||
|
history_stream: impl futures::Stream<Item = Result<SsbMessageKVT, GolgiError>>,
|
||||||
|
) -> Vec<Post> {
|
||||||
|
let mut posts = Vec::new();
|
||||||
|
|
||||||
|
futures::pin_mut!(history_stream);
|
||||||
|
|
||||||
|
while let Some(res) = history_stream.next().await {
|
||||||
|
match res {
|
||||||
|
Ok(msg) => {
|
||||||
|
if msg.value.is_message_type(SsbMessageContentType::Post) {
|
||||||
|
let content = msg.value.content.to_owned();
|
||||||
|
if let Value::Object(map) = content {
|
||||||
|
if !map.contains_key("root") {
|
||||||
|
let text = map.get_key_value("text").unwrap();
|
||||||
|
let timestamp_int = msg.value.timestamp.round() as i64 / 1000;
|
||||||
|
//let timestamp = Utc.timestamp(timestamp_int, 0);
|
||||||
|
let datetime = NaiveDateTime::from_timestamp(timestamp_int, 0);
|
||||||
|
//let datetime = timestamp.to_rfc2822();
|
||||||
|
let date = datetime.format("%d %b %Y").to_string();
|
||||||
|
posts.push(Post {
|
||||||
|
key: msg.key.to_owned(),
|
||||||
|
text: text.1.to_string(),
|
||||||
|
date,
|
||||||
|
sequence: msg.value.sequence,
|
||||||
|
read: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Print the `GolgiError` of this element to `stderr`.
|
||||||
|
warn!("err: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
use async_std::{channel::Receiver, task};
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::{db::Database, sbot};
|
||||||
|
|
||||||
|
pub enum Task {
|
||||||
|
Cancel,
|
||||||
|
FetchAll(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn(rx: Receiver<Task>, db: Database) {
|
||||||
|
task::spawn(async move {
|
||||||
|
while let Ok(task) = rx.recv().await {
|
||||||
|
match task {
|
||||||
|
// Fetch all messages authored by the given peer, filter
|
||||||
|
// the root posts and insert them into the peer tree of the
|
||||||
|
// database.
|
||||||
|
Task::FetchAll(peer) => {
|
||||||
|
let peer_msgs = sbot::get_message_stream(&peer).await;
|
||||||
|
let root_posts = sbot::get_root_posts(peer_msgs).await;
|
||||||
|
match db.insert_post_batch(&peer, root_posts) {
|
||||||
|
Ok(_) => debug!("inserted message batch into peer tree for {}", &peer),
|
||||||
|
Err(e) => warn!(
|
||||||
|
"failed to insert message batch into peer tree for {}: {}",
|
||||||
|
&peer, e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::Cancel => {
|
||||||
|
info!("exiting task loop...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<a href="https://www.flaticon.com/free-icons/download" title="download icons">Download icons created by Kiranshastry - Flaticon</a>
|
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,131 @@
|
||||||
|
<!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">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav {
|
||||||
|
grid-area: nav;
|
||||||
|
border: 5px solid #19A974;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peers {
|
||||||
|
grid-area: peers;
|
||||||
|
border: 5px solid #357EDD;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts {
|
||||||
|
grid-area: posts;
|
||||||
|
border: 5px solid #FF6300;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
border: 5px solid #FFD700;
|
||||||
|
border-radius: 15px;
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-template-rows: 1fr 200px 4fr;
|
||||||
|
grid-template-areas:
|
||||||
|
'nav nav nav nav nav'
|
||||||
|
'peers posts posts posts posts'
|
||||||
|
'peers content content content content';
|
||||||
|
grid-gap: 10px;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-top: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container > div {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
/* text-align: center; */
|
||||||
|
padding: 20px 0;
|
||||||
|
/* font-size: 30px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-container > input {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { text-decoration: none; color: black; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="container">
|
||||||
|
<a href="/"><h1 style="margin-left: 15px;">lykin</h1></a>
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="nav">
|
||||||
|
<div class="flex-container">
|
||||||
|
<a href="/" style="margin-left: 20px;"><img src="/icons/download.png" style="width: 55px;"></a>
|
||||||
|
<a href="/" style="margin-left: 20px;"><img src="/icons/read_post.png" style="width: 55px;"></a>
|
||||||
|
<a href="/" style="margin-left: 20px;"><img src="/icons/unread_post.png" style="width: 55px;"></a>
|
||||||
|
<a href="/" style="margin-left: 20px;"><img src="/icons/delete_post.png" style="width: 55px;"></a>
|
||||||
|
<form class="flex-container" style="margin-left: auto; margin-right: 10px;" action="/subscribe" method="post">
|
||||||
|
<label for="public_key">Public Key</label>
|
||||||
|
<input type="text" id="public_key" name="public_key" size=50 maxlength=53>
|
||||||
|
<input type="submit" value="Subscribe">
|
||||||
|
<input type="submit" value="Unsubscribe" formaction="/unsubscribe">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="peers" style="text-align: center;">
|
||||||
|
<ul style="padding-left: 0;">
|
||||||
|
{% for peer in peers -%}
|
||||||
|
<li style="list-style: none; font-size: 12px;">
|
||||||
|
<a href="/posts/{{ peer | replace(from="/", to="%2F") }}">
|
||||||
|
<code style="word-wrap: anywhere;{% if selected_peer and peer == selected_peer %} font-weight: bold;{% endif %}">{{ peer }}</code>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="posts">
|
||||||
|
{% if posts %}
|
||||||
|
<ul style="padding-left: 25px;">
|
||||||
|
{% for post in posts -%}
|
||||||
|
<li class="flex" style="list-style: none; font-size: 12px;">
|
||||||
|
<a href="/posts/{{ selected_peer | replace(from="/", to="%2F") }}/{{ post.key | replace(from="/", to="%2F") }}">
|
||||||
|
<code style="word-wrap: anywhere;{% if selected_post and post.key == selected_post %} font-weight: bold;{% endif %}">{{ post.key }}</code>
|
||||||
|
| {{ post.date }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{% if post %}
|
||||||
|
{{ post.text }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue