From e1087a1a9c7e8d9365529167a26ffb2fe8e582fe Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Wed, 27 Nov 2024 20:24:20 +0100 Subject: [PATCH] Improve pubky app post --- src/lib.rs | 2 + src/post.rs | 195 +++++++++++++++++++++++++++++++++++++++++++++++-- src/tag.rs | 38 +++++----- src/traits.rs | 6 +- src/version.rs | 3 + 5 files changed, 217 insertions(+), 27 deletions(-) create mode 100644 src/version.rs diff --git a/src/lib.rs b/src/lib.rs index f59fb3e..634aee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ mod post; mod tag; pub mod traits; mod user; +mod version; pub use bookmark::PubkyAppBookmark; pub use file::PubkyAppFile; @@ -14,3 +15,4 @@ pub use mute::PubkyAppMute; pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; pub use tag::PubkyAppTag; pub use user::{PubkyAppUser, PubkyAppUserLink}; +pub use version::{APP_PATH, PROTOCOL, VERSION}; diff --git a/src/post.rs b/src/post.rs index f027dca..c0937a6 100644 --- a/src/post.rs +++ b/src/post.rs @@ -1,4 +1,7 @@ -use crate::traits::{TimestampId, Validatable}; +use crate::{ + traits::{HasPath, TimestampId, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; use std::fmt; use url::Url; @@ -10,7 +13,7 @@ const MAX_LONG_CONTENT_LENGTH: usize = 50000; /// Represents the type of pubky-app posted data /// Used primarily to best display the content in UI -#[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum PubkyAppPostKind { #[default] @@ -32,11 +35,11 @@ impl fmt::Display for PubkyAppPostKind { } } -/// Used primarily to best display the content in UI +/// Represents embedded content within a post #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPostEmbed { - kind: PubkyAppPostKind, // If a repost: `short`, and uri of the reposted post. - uri: String, + kind: PubkyAppPostKind, // Kind of the embedded content + uri: String, // URI of the embedded content } /// Represents raw post in homeserver with content and kind @@ -55,8 +58,34 @@ pub struct PubkyAppPost { attachments: Option>, } +impl PubkyAppPost { + /// Creates a new `PubkyAppPost` instance and sanitizes it. + pub fn new( + content: String, + kind: PubkyAppPostKind, + parent: Option, + embed: Option, + attachments: Option>, + ) -> Self { + let post = PubkyAppPost { + content, + kind, + parent, + embed, + attachments, + }; + post.sanitize() + } +} + impl TimestampId for PubkyAppPost {} +impl HasPath for PubkyAppPost { + fn get_path(&self) -> String { + format!("{}posts/{}", APP_PATH, self.create_id()) + } +} + impl Validatable for PubkyAppPost { fn sanitize(self) -> Self { // Sanitize content @@ -134,8 +163,162 @@ impl Validatable for PubkyAppPost { _ => (), }; - // TODO: additional validation? + // TODO: additional validation. Attachement URLs...? Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use bytes::Bytes; + + #[test] + fn test_create_id() { + let post = PubkyAppPost::new( + "Hello World!".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let post_id = post.create_id(); + println!("Generated Post ID: {}", post_id); + + // Assert that the post ID is 13 characters long + assert_eq!(post_id.len(), 13); + } + + #[test] + fn test_new() { + let content = "This is a test post".to_string(); + let kind = PubkyAppPostKind::Short; + let post = PubkyAppPost::new(content.clone(), kind.clone(), None, None, None); + + assert_eq!(post.content, content); + assert_eq!(post.kind, kind); + assert!(post.parent.is_none()); + assert!(post.embed.is_none()); + assert!(post.attachments.is_none()); + } + + #[test] + fn test_get_path() { + let post = PubkyAppPost::new( + "Test post".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let post_id = post.create_id(); + let expected_path_len = format!("{}posts/{}", APP_PATH, post_id).len(); + let path = post.get_path(); + + assert_eq!(path.len(), expected_path_len); + } + + #[test] + fn test_sanitize() { + let content = " This is a test post with extra whitespace ".to_string(); + let post = PubkyAppPost::new( + content.clone(), + PubkyAppPostKind::Short, + Some("invalid uri".to_string()), + Some(PubkyAppPostEmbed { + kind: PubkyAppPostKind::Link, + uri: "invalid uri".to_string(), + }), + None, + ); + + let sanitized_post = post.sanitize(); + assert_eq!(sanitized_post.content, content.trim()); + assert!(sanitized_post.parent.is_none()); + assert!(sanitized_post.embed.is_none()); + } + + #[test] + fn test_validate_valid() { + let post = PubkyAppPost::new( + "Valid content".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let id = post.create_id(); + let result = post.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let post = PubkyAppPost::new( + "Valid content".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let invalid_id = "INVALIDID12345"; + let result = post.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let post_json = r#" + { + "content": "Hello World!", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null + } + "#; + + let id = PubkyAppPost::new( + "Hello World!".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ) + .create_id(); + + let blob = Bytes::from(post_json); + let post = ::try_from(&blob, &id).unwrap(); + + assert_eq!(post.content, "Hello World!"); + } + + #[test] + fn test_try_from_invalid_content() { + let content = "[DELETED]".to_string(); + let post_json = format!( + r#"{{ + "content": "{}", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null + }}"#, + content + ); + + let id = PubkyAppPost::new(content.clone(), PubkyAppPostKind::Short, None, None, None) + .create_id(); + + let blob = Bytes::from(post_json); + let post = ::try_from(&blob, &id).unwrap(); + + assert_eq!(post.content, "empty"); // After sanitization + } +} diff --git a/src/tag.rs b/src/tag.rs index 42b5055..e66f910 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,6 +1,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use crate::traits::{HasPath, HashId, Validatable}; +use crate::{ + traits::{HasPath, HashId, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; use url::Url; @@ -17,9 +20,9 @@ const MAX_TAG_LABEL_LENGTH: usize = 20; /// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) #[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppTag { - pub uri: String, - pub label: String, - pub created_at: i64, + uri: String, + label: String, + created_at: i64, } impl PubkyAppTag { @@ -40,7 +43,7 @@ impl PubkyAppTag { impl HasPath for PubkyAppTag { fn get_path(&self) -> String { - format!("pubky:///pub/pubky.app/tags/{}", self.create_id()) + format!("{}tags/{}", APP_PATH, self.create_id()) } } @@ -101,7 +104,7 @@ impl Validatable for PubkyAppTag { #[cfg(test)] mod tests { use super::*; - use crate::traits::Validatable; + use crate::{traits::Validatable, APP_PATH}; use bytes::Bytes; #[test] @@ -133,6 +136,8 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_micros() as i64; + println!("TIMESTAMP {}", tag.created_at); + println!("TIMESTAMP {}", now); assert!(tag.created_at <= now && tag.created_at >= now - 1_000_000); // within 1 second } @@ -140,13 +145,13 @@ mod tests { #[test] fn test_get_path() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0032FNCGXE3R0".to_string(), created_at: 1627849723000, label: "cool".to_string(), }; let expected_id = tag.create_id(); - let expected_path = format!("pubky:///pub/pubky.app/tags/{}", expected_id); + let expected_path = format!("{}tags/{}", APP_PATH, expected_id); let path = tag.get_path(); assert_eq!(path, expected_path); @@ -155,7 +160,7 @@ mod tests { #[test] fn test_sanitize() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: " CoOl ".to_string(), created_at: 1627849723000, }; @@ -167,7 +172,7 @@ mod tests { #[test] fn test_validate_valid() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: "cool".to_string(), created_at: 1627849723000, }; @@ -180,7 +185,7 @@ mod tests { #[test] fn test_validate_invalid_label_length() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), created_at: 1627849723000, }; @@ -197,7 +202,7 @@ mod tests { #[test] fn test_validate_invalid_id() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: "cool".to_string(), created_at: 1627849723000, }; @@ -212,24 +217,21 @@ mod tests { fn test_try_from_valid() { let tag_json = r#" { - "uri": "pubky://user_pubky_id/pub/pubky.app/v1/profile.json", + "uri": "pubky://user_pubky_id/pub/pubky.app/profile.json", "label": "Cool Tag", "created_at": 1627849723000 } "#; let id = PubkyAppTag::new( - "pubky://user_pubky_id/pub/pubky.app/v1/profile.json".to_string(), + "pubky://user_pubky_id/pub/pubky.app/profile.json".to_string(), "Cool Tag".to_string(), ) .create_id(); let blob = Bytes::from(tag_json); let tag = ::try_from(&blob, &id).unwrap(); - assert_eq!( - tag.uri, - "pubky://user_pubky_id/pub/pubky.app/v1/profile.json" - ); + assert_eq!(tag.uri, "pubky://user_pubky_id/pub/pubky.app/profile.json"); assert_eq!(tag.label, "cool tag"); // After sanitization } diff --git a/src/traits.rs b/src/traits.rs index 07ced6e..04afe29 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -11,7 +11,7 @@ pub trait TimestampId { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") - .as_micros(); + .as_micros() as u64; // Convert to big-endian bytes let bytes = now.to_be_bytes(); @@ -43,7 +43,7 @@ pub trait TimestampId { let now_micros = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") - .as_micros(); + .as_micros() as u64; // Define October 1st, 2024, in microseconds since UNIX epoch let oct_first_2024_micros = 1727740800000000u64; // Timestamp for 2024-10-01 00:00:00 UTC @@ -59,7 +59,7 @@ pub trait TimestampId { } // Validate that the ID's timestamp is not more than 2 hours in the future - if timestamp_micros as u128 > max_future_micros { + if timestamp_micros > max_future_micros { return Err("Validation Error: Invalid ID, timestamp is too far in the future".into()); } diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..bac70cd --- /dev/null +++ b/src/version.rs @@ -0,0 +1,3 @@ +pub static VERSION: &str = "0.2.0"; +pub static APP_PATH: &str = "/pub/pubky.app/"; +pub static PROTOCOL: &str = "pubky://";