You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
7.1 KiB
247 lines
7.1 KiB
use super::*;
|
|
use cryptohelpers::sha256::Sha256Hash;
|
|
|
|
mod render;
|
|
pub use render::*;
|
|
|
|
use hard_format::formats::{
|
|
MaxLenString,
|
|
Base64FormattedString,
|
|
Base64FormattedStr,
|
|
self,
|
|
};
|
|
use mnemonic::Tripcode;
|
|
|
|
id_type!(PostID; "A unique post ID");
|
|
|
|
/// String type that limits its bytes to the ID string max limit.
|
|
pub type IDMaxString = MaxLenString<{defaults::POST_ID_MAX_LEN}>;
|
|
|
|
/// The timestamp type used in posts
|
|
pub type PostTimestamp = chrono::DateTime<defaults::Timezone>;
|
|
|
|
/// A size limited base64 formatting specifier
|
|
type PostBodyFormat = formats::BothFormat<formats::MaxLenFormat<{defaults::POST_BODY_MAX_SIZE}>, formats::Base64Format>;
|
|
|
|
/// A size limited base64 string
|
|
pub type PostBodyString = hard_format::FormattedString<PostBodyFormat>;
|
|
/// A size limited base64 string
|
|
pub type PostBodyStr = hard_format::FormattedStr<PostBodyFormat>;
|
|
|
|
/// Identifiers for a post
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
|
pub struct Ident
|
|
{
|
|
name: Option<IDMaxString>,
|
|
tripcode: Option<Tripcode>,
|
|
email: Option<IDMaxString>,
|
|
}
|
|
|
|
impl Ident
|
|
{
|
|
/// Create a new ident object
|
|
///
|
|
/// # Panics
|
|
/// If `name` or `email` are longer than `defaults::POST_ID_MAX_LEN`.
|
|
pub fn new(name: Option<&str>, tripcode: Option<Tripcode>, email: Option<&str>) -> Self
|
|
{
|
|
Self {
|
|
name: name.map(|x| IDMaxString::new(x.to_owned()).expect("Name too long")),
|
|
email: email.map(|x| IDMaxString::new(x.to_owned()).expect("Email too long")),
|
|
tripcode
|
|
}
|
|
}
|
|
|
|
/// The name of this user ident
|
|
pub fn name(&self) -> &str
|
|
{
|
|
self.name.as_ref().map(|x| x.as_str()).unwrap_or(defaults::ANON_NAME)
|
|
}
|
|
/// The tripcode of this user ident
|
|
pub fn tripcode(&self) -> Option<&Tripcode>
|
|
{
|
|
self.tripcode.as_ref()
|
|
}
|
|
/// The email of this user ident
|
|
pub fn email(&self) -> Option<&str>
|
|
{
|
|
self.email.as_ref().map(|x| x.as_str())
|
|
}
|
|
}
|
|
|
|
|
|
/// A single completed post.
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
pub struct Post
|
|
{
|
|
/// Unique ID for each post
|
|
id: PostID,
|
|
|
|
/// Identifiers for this post
|
|
ident: Ident,
|
|
|
|
/// The client-side generated AES key used for decrypting the body's text.
|
|
///
|
|
/// This AES key can be encrypted with any number of RSA public keys on the client side. There must be at least one, or the post body is lost and the post should be removed.
|
|
body_aes_keys: Vec<PostBodyString>,
|
|
|
|
/// The client-side encrypted body string
|
|
body: PostBodyString,
|
|
|
|
/// Signature of the body (optional).
|
|
signature: Option<Base64FormattedString>,
|
|
|
|
/// 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<PostTimestamp>,
|
|
|
|
/// Optional dynamic expiry duration.
|
|
expires_in: Option<tokio::time::Duration>,
|
|
}
|
|
|
|
// Time based functions
|
|
impl Post
|
|
{
|
|
/// The timestamp when the post should expire.
|
|
///
|
|
/// # Notes
|
|
/// If the conversion overflows, then the expiry time is treated as 0. (i.e. the post expires immediately).
|
|
pub fn expires_at(&self) -> PostTimestamp
|
|
{
|
|
self.created + chrono::Duration::from_std(self.expires_in.unwrap_or(defaults::POST_EXPIRE))
|
|
.unwrap_or(chrono::Duration::zero())
|
|
}
|
|
/// How long until the post has until it reaches its expiry time from its creation time.
|
|
///
|
|
/// If this value is *lower* than the value of `time_since_creation`, then the post has expired.
|
|
pub fn expires_in(&self) -> chrono::Duration
|
|
{
|
|
self.expires_at() - self.created
|
|
}
|
|
/// Time passed since this post was created as a Chrono `Duration`.
|
|
pub fn time_since_creation(&self) -> chrono::Duration
|
|
{
|
|
defaults::Timezone::now() - self.created
|
|
}
|
|
/// The timestamp for when this post was created
|
|
pub fn created(&self) -> PostTimestamp
|
|
{
|
|
self.created
|
|
}
|
|
/// A slice of timestamps showing when this post was edited, in order of those edits.
|
|
pub fn edited(&self) -> &[PostTimestamp]
|
|
{
|
|
&self.edited[..]
|
|
}
|
|
/// 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
|
|
}
|
|
}
|
|
}
|
|
// Ident based functions
|
|
impl Post
|
|
{
|
|
/// Get a mnemonic for this post's ID.
|
|
#[inline] pub fn post_id_mnemonic(&self) -> mnemonic::MnemonicHash
|
|
{
|
|
mnemonic::MnemonicHash::from_slice(self.id.id_as_bytes())
|
|
}
|
|
/// This post's unique identifier
|
|
#[inline] pub fn post_id(&self) -> &PostID
|
|
{
|
|
&self.id
|
|
}
|
|
/// The user-set name for this post if there is one.
|
|
#[inline] pub fn own_name(&self) -> Option<&str>
|
|
{
|
|
self.ident.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.ident.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.ident.tripcode.as_ref()
|
|
}
|
|
|
|
/// The AES encrypted body of this post
|
|
pub fn body(&self) -> &PostBodyStr
|
|
{
|
|
self.body.as_ref()
|
|
}
|
|
|
|
/// An iterator of RSA ciphertexts of the AES key used to encrypt the body text.
|
|
pub fn body_keys<'a>(&'a self) -> impl Iterator<Item = &'a PostBodyStr> + ExactSizeIterator + 'a
|
|
{
|
|
self.body_aes_keys.iter().map(|x| x.as_ref())
|
|
}
|
|
|
|
/// The PEM formatted signature of this post, if there is one.
|
|
pub fn signature(&self) -> Option<&Base64FormattedStr>
|
|
{
|
|
self.signature.as_ref().map(|x| x.as_ref())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests
|
|
{
|
|
#[test]
|
|
fn post_serialise()
|
|
{
|
|
use std::convert::TryInto;
|
|
let post = super::Post {
|
|
id: super::PostID::id_new(),
|
|
ident: super::Ident {
|
|
name: Some("Some name".to_owned().try_into().unwrap()),
|
|
email: None,
|
|
tripcode: Some(super::Tripcode::generate("uhh hello").unwrap()),
|
|
},
|
|
body: super::PostBodyString::new(crate::hard_format::formats::Base64FormattedString::encode("Hello world").into()).unwrap(),
|
|
body_aes_keys: vec![super::PostBodyString::new(crate::hard_format::formats::Base64FormattedString::encode("TODO").into()).unwrap()],
|
|
signature: None,
|
|
hash: Default::default(),
|
|
|
|
created: crate::defaults::Timezone::now(),
|
|
edited: Default::default(),
|
|
expires_in: None,
|
|
};
|
|
eprintln!("Post as html: {}", html! { body { (post) } }.into_string());
|
|
println!("Post is: {:?}", post);
|
|
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: {:?}", post2);
|
|
}
|
|
}
|