use std::path::Path; use log::{debug, info}; use serde::{Deserialize, Serialize}; use sled::{Batch, Db, IVec, Result, Tree}; /// Scuttlebutt peer data. #[derive(Debug, Deserialize, Serialize)] pub struct Peer { pub public_key: String, pub name: String, pub latest_sequence: u64, } impl Peer { /// Create a new instance of the Peer struct using the given public /// key. A default value is set for name. pub fn new(public_key: &str) -> Peer { Peer { public_key: public_key.to_string(), name: "".to_string(), latest_sequence: 0, } } /// Modify the name field of an instance of the Peer struct, leaving /// the other values unchanged. pub fn set_name(self, name: &str) -> Peer { Self { name: name.to_string(), ..self } } /// Modify the latest_sequence field of an instance of the Peer struct, /// leaving the other values unchanged. pub fn set_latest_sequence(self, latest_sequence: u64) -> Peer { Self { latest_sequence, ..self } } } /// The text and metadata of a Scuttlebutt root post. #[derive(Debug, Deserialize, Serialize)] pub struct Post { /// The key of the post-type message, also known as a message reference. pub key: String, /// The text of the post (may be formatted as markdown). pub text: String, /// The date the post was published (e.g. 17 May 2021). pub date: String, /// The sequence number of the post-type message. pub sequence: u64, /// The read state of the post; true if read, false if unread. pub read: bool, /// The timestamp representing the date the post was published. pub timestamp: i64, /// The subject of the post, represented as the first 53 characters of /// the post text. pub subject: Option, } impl Post { // Create a new instance of the Post struct. A default value of `false` is // set for `read`. pub fn new( key: String, text: String, date: String, sequence: u64, timestamp: i64, subject: Option, ) -> Post { Post { key, text, date, sequence, timestamp, subject, read: false, } } } /// An instance of the key-value database and relevant trees. #[allow(dead_code)] #[derive(Clone)] pub struct Database { /// The sled database instance. db: Db, /// A database tree containing Peer struct instances for all the peers /// we are subscribed to. peer_tree: Tree, /// A database tree containing Post struct instances for all of the posts /// we have downloaded from the peer to whom we subscribe. pub post_tree: Tree, } impl Database { /// Initialise the database by opening the database file, loading the /// peers tree and returning an instantiated Database struct. 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 sled database"); let db = sled::open(path).expect("Failed to open database"); debug!("Opening 'peers' database tree"); let peer_tree = db .open_tree("peers") .expect("Failed to open 'peers' database tree"); debug!("Opening 'posts' database tree"); let post_tree = db .open_tree("posts") .expect("Failed to open 'posts' database tree"); Database { db, peer_tree, post_tree, } } /// Add a peer to the database by inserting the public key into the peer /// tree. pub fn add_peer(&self, peer: Peer) -> Result> { debug!("Serializing peer data for {} to bincode", &peer.public_key); let peer_bytes = bincode::serialize(&peer).unwrap(); debug!( "Inserting peer {} into 'peers' database tree", &peer.public_key ); self.peer_tree.insert(&peer.public_key, peer_bytes) } /// Get a single peer from the peer tree, defined by the given public key. /// The byte value for the matching entry, if found, is deserialized from /// bincode into an instance of the Peer struct. pub fn get_peer(&self, public_key: &str) -> Result> { debug!( "Retrieving peer data for {} from 'peers' database tree", &public_key ); let peer = self .peer_tree .get(public_key.as_bytes()) .unwrap() .map(|peer| { debug!("Deserializing peer data for {} from bincode", &public_key); bincode::deserialize(&peer).unwrap() }); Ok(peer) } /// Get a list of all peers in the peer tree. The byte value for each /// peer entry is deserialized from bincode into an instance of the Peer /// struct. pub fn get_peers(&self) -> Vec { debug!("Retrieving data for all peers in the 'peers' database tree"); let mut peers = Vec::new(); self.peer_tree .iter() .map(|peer| peer.unwrap()) .for_each(|peer| { debug!( "Deserializing peer data for {} from bincode", String::from_utf8_lossy(&peer.0).into_owned() ); peers.push(bincode::deserialize(&peer.1).unwrap()) }); peers } /// Remove a peer from the database, as represented by the given public /// key. pub fn remove_peer(&self, public_key: &str) -> Result<()> { debug!("Removing peer {} from 'peers' database tree", &public_key); self.peer_tree.remove(&public_key).map(|_| ()) } /// Add a post to the database by inserting an instance of the Post struct /// into the post tree. pub fn add_post(&self, public_key: &str, post: Post) -> Result> { let post_key = format!("{}_{}", public_key, post.key); debug!("Serializing post data for {} to bincode", &post_key); let post_bytes = bincode::serialize(&post).unwrap(); debug!("Inserting post {} into 'posts' database tree", &post_key); self.post_tree.insert(post_key.as_bytes(), post_bytes) } /// Add a batch of posts to the database by inserting a vector of instances /// of the Post struct into the post tree. pub fn add_post_batch(&self, public_key: &str, posts: Vec) -> Result<()> { let mut post_batch = Batch::default(); for post in posts { let post_key = format!("{}_{}", public_key, post.key); debug!("Serializing post data for {} to bincode", &post_key); let post_bytes = bincode::serialize(&post).unwrap(); debug!("Inserting post {} into 'posts' database tree", &post_key); post_batch.insert(post_key.as_bytes(), post_bytes) } debug!("Applying batch insertion into 'posts' database tree"); self.post_tree.apply_batch(post_batch) } /// Get a list of all posts in the post tree authored by the given public /// key and sort them by timestamp in descending order. The byte value for /// each matching entry is deserialized from bincode into an instance of /// the Post struct. pub fn get_posts(&self, public_key: &str) -> Result> { debug!("Retrieving data for all posts in the 'posts' database tree"); let mut posts = Vec::new(); self.post_tree .scan_prefix(public_key.as_bytes()) .map(|post| post.unwrap()) .for_each(|post| { debug!( "Deserializing post data for {} from bincode", String::from_utf8_lossy(&post.0).into_owned() ); posts.push(bincode::deserialize(&post.1).unwrap()) }); posts.sort_by(|a: &Post, b: &Post| b.timestamp.cmp(&a.timestamp)); Ok(posts) } /// Get a single post from the post tree, authored by the given public key /// and defined by the given message ID. The byte value for the matching /// entry, if found, is deserialized from bincode into an instance of the /// Post struct. pub fn get_post(&self, public_key: &str, msg_id: &str) -> Result> { let post_key = format!("{}_{}", public_key, msg_id); debug!( "Retrieving post data for {} from 'posts' database tree", &post_key ); let post = self .post_tree .get(post_key.as_bytes()) .unwrap() .map(|post| { debug!("Deserializing post data for {} from bincode", &post_key); bincode::deserialize(&post).unwrap() }); Ok(post) } /// Remove a single post from the post tree, authored by the given public /// key and defined by the given message ID. pub fn remove_post(&self, public_key: &str, msg_id: &str) -> Result<()> { let post_key = format!("{}_{}", public_key, msg_id); debug!("Removing post {} from 'posts' database tree", &post_key); // .remove() would ordinarily return the value of the deleted entry // as an Option, returning None if the post_key was not found. // We don't care about the value of the deleted entry so we simply // map the Option to (). self.post_tree.remove(post_key.as_bytes()).map(|_| ()) } /// Sum the total number of unread posts for the peer represented by the /// given public key. pub fn get_unread_post_count(&self, public_key: &str) -> u16 { debug!( "Counting total number of unread posts for peer {}", &public_key ); let mut unread_post_counter = 0; self.post_tree .scan_prefix(public_key.as_bytes()) .map(|post| post.unwrap()) .for_each(|post| { debug!( "Deserializing post data for {} from bincode", String::from_utf8_lossy(&post.0).into_owned() ); let deserialized_post: Post = bincode::deserialize(&post.1).unwrap(); if !deserialized_post.read { unread_post_counter += 1 } }); unread_post_counter } }