use super::*; use cryptohelpers::sha256::Sha256Hash; use hard_format::formats::{ PEMFormattedString, PEMFormattedStr, MaxLenString, }; use tripcode::Tripcode; id_type!(PostID; "A unique post ID"); /// Max length of `email` or `name` feild const ID_MAX_LEN: usize = defaults::POST_ID_MAX_LEN; /// String type that limits its bytes to the ID string max limit. pub type IDMaxString = MaxLenString; /// The timestamp type used in posts pub type PostTimestamp = chrono::DateTime; /// A single completed post #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct Post { /// Unique ID for each post id: PostID, name: Option, tripcode: Option, email: Option, /// The client-side encrypted body string body: PEMFormattedString, /// Signature of the body (optional). signature: Option, /// Hash of the body hash: Sha256Hash, /// When the post was created created: PostTimestamp, /// When the post was last edited. /// /// # Notes /// Each new edit is pushed to the end of the vec, creation does not count as an edit. edited: Vec, /// Optional dynamic expiry duration. expires_in: Option, } impl Post { /// Time since this post was created as a Chrono `Duration`. pub fn time_since_creation(&self) -> chrono::Duration { defaults::Timezone::now() - self.created } /// Has this post expired? /// /// Expired posts should be removed pub fn expired(&self) -> bool { if let Ok(dur) = self.time_since_creation().to_std() { dur >= *self.expires_in.as_ref().unwrap_or(&defaults::POST_EXPIRE) } else { // Conversion failed. Expire the post true } } /// The user-set name for this post if there is one. #[inline] pub fn own_name(&self) -> Option<&str> { self.name.as_ref().map(|x| x.as_str()) } /// The name for this post. /// /// If no name is set, returns the default anon name. pub fn name(&self) -> &str { self.own_name().unwrap_or(defaults::ANON_NAME) } /// The email set for this post, if there is one. pub fn email(&self) -> Option<&str> { self.email.as_ref().map(|x| x.as_str()) } /// Get the tripcode of this post, if there is one. pub fn tripcode(&self) -> Option<&Tripcode> { self.tripcode.as_ref() } /// The body of this post pub fn body(&self) -> &PEMFormattedStr { self.body.as_ref() } } #[cfg(test)] mod tests { #[test] fn post_serialise() { use std::convert::TryInto; let post = super::Post { id: super::PostID::id_new(), name: Some("Some name".to_owned().try_into().unwrap()), email: None, tripcode: Some(super::Tripcode::generate("uhh hello").unwrap()), body: unsafe { super::PEMFormattedStr::new_unchecked("test").to_owned() }, //temporary signature: None, hash: Default::default(), created: crate::defaults::Timezone::now(), edited: Default::default(), expires_in: None, }; let post_json = serde_json::to_vec(&post).expect("Serialise"); println!("Post json: {}", std::str::from_utf8(&post_json[..]).unwrap()); let post2: super::Post = serde_json::from_slice(&post_json[..]).expect("Deserialise"); assert_eq!(post, post2); println!("Post was: {:?}", post); } }