diff --git a/Cargo.lock b/Cargo.lock index 6c3e753..3a8cf00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -77,6 +92,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "base32" version = "0.5.1" @@ -520,6 +550,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hash32" version = "0.2.1" @@ -549,6 +585,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "httpdate" version = "1.0.3" @@ -818,6 +860,27 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -836,6 +899,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -848,6 +920,29 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -968,6 +1063,7 @@ dependencies = [ "pubky-common", "serde", "serde_json", + "tokio", "url", "utoipa", ] @@ -1047,6 +1143,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1163,6 +1274,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -1196,6 +1316,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.9.8" @@ -1279,6 +1409,35 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing" version = "0.1.40" @@ -1476,6 +1635,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index dce5752..3a9f313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ url = "2.5.4" base32 = "0.5.1" blake3 = "1.5.4" chrono = "0.4.38" + +[dev-dependencies] +tokio = { version = "1.41.1", features = ["full"] } diff --git a/src/bookmark.rs b/src/bookmark.rs index 1b92cdd..d02db3c 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize}; /// Where bookmark_id is Crockford-base32(Blake3("{uri_bookmarked}"")[:half]) #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppBookmark { - pub uri: String, - pub created_at: i64, + uri: String, + created_at: i64, } #[async_trait] diff --git a/src/file.rs b/src/file.rs index 1d69c62..900daac 100644 --- a/src/file.rs +++ b/src/file.rs @@ -6,11 +6,11 @@ use serde::{Deserialize, Serialize}; /// Profile schema #[derive(Deserialize, Serialize, Debug)] pub struct PubkyAppFile { - pub name: String, - pub created_at: i64, - pub src: String, - pub content_type: String, - pub size: i64, + name: String, + created_at: i64, + src: String, + content_type: String, + size: i64, } impl TimestampId for PubkyAppFile {} diff --git a/src/follow.rs b/src/follow.rs index d14ff83..5ccc22f 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppFollow { - pub created_at: i64, + created_at: i64, } #[async_trait] diff --git a/src/lib.rs b/src/lib.rs index a768d44..4b2cd5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,17 @@ -pub mod bookmark; -pub mod file; -pub mod follow; -pub mod mute; -pub mod post; -pub mod tag; +mod bookmark; +mod file; +mod follow; +mod mute; +mod post; +mod tag; pub mod traits; -pub mod types; -pub mod user; +mod types; +mod user; pub use bookmark::PubkyAppBookmark; pub use file::PubkyAppFile; pub use follow::PubkyAppFollow; pub use mute::PubkyAppMute; -pub use post::{PostEmbed, PostKind, PubkyAppPost}; +pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; pub use tag::PubkyAppTag; -pub use user::{PubkyAppUser, UserLink}; +pub use user::{PubkyAppUser, PubkyAppUserLink}; diff --git a/src/mute.rs b/src/mute.rs index 20b6f8e..b0d546d 100644 --- a/src/mute.rs +++ b/src/mute.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppMute { - pub created_at: i64, + created_at: i64, } #[async_trait] diff --git a/src/post.rs b/src/post.rs index 2ebc770..f5614e0 100644 --- a/src/post.rs +++ b/src/post.rs @@ -14,7 +14,7 @@ const MAX_LONG_CONTENT_LENGTH: usize = 50000; /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone)] #[serde(rename_all = "lowercase")] -pub enum PostKind { +pub enum PubkyAppPostKind { #[default] Short, Long, @@ -24,7 +24,7 @@ pub enum PostKind { File, } -impl fmt::Display for PostKind { +impl fmt::Display for PubkyAppPostKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let string_repr = serde_json::to_value(self) .ok() @@ -36,9 +36,9 @@ impl fmt::Display for PostKind { /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, Default, Clone)] -pub struct PostEmbed { - pub kind: PostKind, - pub uri: String, // If a repost a `Short` and uri of the reposted post. +pub struct PubkyAppPostEmbed { + kind: PubkyAppPostKind, + uri: String, // If a repost a `Short` and uri of the reposted post. } /// Represents raw post in homeserver with content and kind @@ -50,11 +50,11 @@ pub struct PostEmbed { /// `/pub/pubky.app/posts/00321FCW75ZFY` #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPost { - pub content: String, - pub kind: PostKind, - pub parent: Option, // If a reply, the URI of the parent post. - pub embed: Option, - pub attachments: Option>, + content: String, + kind: PubkyAppPostKind, + parent: Option, // If a reply, the URI of the parent post. + embed: Option, + attachments: Option>, } impl TimestampId for PubkyAppPost {} @@ -72,10 +72,10 @@ impl Validatable for PubkyAppPost { content = "empty".to_string() } - // Define content length limits based on PostKind + // Define content length limits based on PubkyAppPostKind let max_content_length = match self.kind { - PostKind::Short => MAX_SHORT_CONTENT_LENGTH, - PostKind::Long => MAX_LONG_CONTENT_LENGTH, + PubkyAppPostKind::Short => MAX_SHORT_CONTENT_LENGTH, + PubkyAppPostKind::Long => MAX_LONG_CONTENT_LENGTH, _ => MAX_SHORT_CONTENT_LENGTH, // Default limit for other kinds }; @@ -94,7 +94,7 @@ impl Validatable for PubkyAppPost { // Sanitize embed if present let embed = if let Some(embed) = &self.embed { match Url::parse(&embed.uri) { - Ok(url) => Some(PostEmbed { + Ok(url) => Some(PubkyAppPostEmbed { kind: embed.kind.clone(), uri: url.to_string(), // Use normalized version }), @@ -119,12 +119,12 @@ impl Validatable for PubkyAppPost { // Validate content length match self.kind { - PostKind::Short => { + PubkyAppPostKind::Short => { if self.content.chars().count() > MAX_SHORT_CONTENT_LENGTH { return Err("Post content exceeds maximum length for Short kind".into()); } } - PostKind::Long => { + PubkyAppPostKind::Long => { if self.content.chars().count() > MAX_LONG_CONTENT_LENGTH { return Err("Post content exceeds maximum length for Short kind".into()); } diff --git a/src/tag.rs b/src/tag.rs index d52b83a..fedbd72 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,6 +1,7 @@ -use crate::traits::{HashId, Validatable}; +use crate::traits::{HasPath, HashId, Validatable}; use crate::types::DynError; use async_trait::async_trait; +use chrono::Utc; use serde::{Deserialize, Serialize}; use url::Url; @@ -15,13 +16,35 @@ const MAX_TAG_LABEL_LENGTH: usize = 20; /// `/pub/pubky.app/tags/FPB0AM9S93Q3M1GFY1KV09GMQM` /// /// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppTag { pub uri: String, pub label: String, pub created_at: i64, } +impl PubkyAppTag { + pub async fn new(uri: String, label: String) -> Self { + let created_at = Utc::now().timestamp_millis(); + let tag = Self { + uri, + label, + created_at, + }; + + match tag.sanitize().await { + Ok(tag) => tag, + Err(_) => Self::default(), + } + } +} + +impl HasPath for PubkyAppTag { + fn get_path(&self) -> String { + format!("pubky:///pub/pubky.app/tags/{}", self.create_id()) + } +} + #[async_trait] impl HashId for PubkyAppTag { /// Tag ID is created based on the hash of the URI tagged and the label used @@ -66,14 +89,155 @@ impl Validatable for PubkyAppTag { } } -#[test] -fn test_create_id() { - let tag = PubkyAppTag { - uri: "user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: "cool".to_string(), - }; +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use bytes::Bytes; + use tokio; + + #[tokio::test] + async fn test_create_id() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + created_at: 1627849723000, + label: "cool".to_string(), + }; + + let tag_id = tag.create_id(); + println!("Generated Tag ID: {}", tag_id); + + // Assert that the tag ID is of expected length + // The length depends on your implementation of create_id + assert!(!tag_id.is_empty()); + } + + #[tokio::test] + async fn test_new() { + let uri = "https://example.com/post/1".to_string(); + let label = "interesting".to_string(); + let tag = PubkyAppTag::new(uri.clone(), label.clone()).await; + + assert_eq!(tag.uri, uri); + assert_eq!(tag.label, label); + // Check that created_at is recent + let now = Utc::now().timestamp_millis(); + assert!(tag.created_at <= now && tag.created_at >= now - 1000); // within 1 second + } + + #[tokio::test] + async fn test_get_path() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".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 path = tag.get_path(); + + assert_eq!(path, expected_path); + } + + #[tokio::test] + async fn test_sanitize() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: " CoOl ".to_string(), + created_at: 1627849723000, + }; + + let sanitized_tag = tag.sanitize().await.unwrap(); + assert_eq!(sanitized_tag.label, "cool"); + } + + #[tokio::test] + async fn test_validate_valid() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: "cool".to_string(), + created_at: 1627849723000, + }; + + let id = tag.create_id(); + let result = tag.validate(&id).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_validate_invalid_label_length() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), + created_at: 1627849723000, + }; + + let id = tag.create_id(); + let result = tag.validate(&id).await; + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Tag label exceeds maximum length" + ); + } + + #[tokio::test] + async fn test_validate_invalid_id() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: "cool".to_string(), + created_at: 1627849723000, + }; + + let invalid_id = "INVALIDID"; + let result = tag.validate(&invalid_id).await; + assert!(result.is_err()); + // You can check the specific error message if necessary + } + + #[tokio::test] + async fn test_try_from_valid() { + let tag_json = r#" + { + "uri": "pubky://user_pubky_id/pub/pubky.app/v1/profile.json", + "label": "Cool Tag", + "created_at": 1627849723000 + } + "#; + + let id = PubkyAppTag::new( + "pubky://user_pubky_id/pub/pubky.app/v1/profile.json".to_string(), + "Cool Tag".to_string(), + ) + .await + .create_id(); + + let blob = Bytes::from(tag_json); + let tag = ::try_from(&blob, &id) + .await + .unwrap(); + assert_eq!( + tag.uri, + "pubky://user_pubky_id/pub/pubky.app/v1/profile.json" + ); + assert_eq!(tag.label, "cool tag"); // After sanitization + } + + #[tokio::test] + async fn test_try_from_invalid_uri() { + let tag_json = r#" + { + "uri": "invalid_uri", + "label": "Cool Tag", + "created_at": 1627849723000 + } + "#; - let tag_id = tag.create_id(); - println!("Generated Tag ID: {}", tag_id); + let id = "SomeID"; // The ID doesn't matter here + let blob = Bytes::from(tag_json); + let result = ::try_from(&blob, &id).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Invalid URI in tag"); + } } diff --git a/src/traits.rs b/src/traits.rs index a4d71ff..0498358 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,8 +1,8 @@ use crate::types::DynError; use async_trait::async_trait; -use bytes::Bytes; use base32::{decode, encode, Alphabet}; use blake3::Hasher; +use bytes::Bytes; use chrono::{DateTime, Duration, NaiveDate, Utc}; use pubky_common::timestamp::Timestamp; use serde::de::DeserializeOwned; @@ -118,3 +118,7 @@ pub trait Validatable: Sized + DeserializeOwned { Ok(self) } } + +pub trait HasPath { + fn get_path(&self) -> String; +} diff --git a/src/user.rs b/src/user.rs index 379800c..5c4982e 100644 --- a/src/user.rs +++ b/src/user.rs @@ -19,18 +19,18 @@ const MAX_STATUS_LENGTH: usize = 50; /// URI: /pub/pubky.app/profile.json #[derive(Deserialize, Serialize, Debug)] pub struct PubkyAppUser { - pub name: String, - pub bio: Option, - pub image: Option, - pub links: Option>, - pub status: Option, + name: String, + bio: Option, + image: Option, + links: Option>, + status: Option, } /// Represents a user's single link with a title and URL. #[derive(Serialize, Deserialize, ToSchema, Default, Clone, Debug)] -pub struct UserLink { - pub title: String, - pub url: String, +pub struct PubkyAppUserLink { + title: String, + url: String, } #[async_trait] @@ -105,7 +105,7 @@ impl Validatable for PubkyAppUser { .collect::(); // Only keep valid URLs - Some(UserLink { title, url }) + Some(PubkyAppUserLink { title, url }) } Err(_) => { None // Discard invalid links